diff options
323 files changed, 9817 insertions, 2941 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/service/quicksettings/TileService.java b/core/java/android/service/quicksettings/TileService.java index 506b3b81eb9a..81c5796374af 100644 --- a/core/java/android/service/quicksettings/TileService.java +++ b/core/java/android/service/quicksettings/TileService.java @@ -506,7 +506,7 @@ public class TileService extends Service { * the calling package or if the calling user cannot act on behalf of the user from the * {@code context}.</li> * <li> {@link IllegalArgumentException} if the user of the {@code context} is not the - * current user.</li> + * current user. Only thrown for apps targeting {@link Build.VERSION_CODES#TIRAMISU}</li> * </ul> */ public static final void requestListeningState(Context context, ComponentName component) { 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/Window.java b/core/java/android/view/Window.java index 02027e4a3969..293f9082670d 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -823,6 +823,11 @@ public abstract class Window { /** @hide */ public final void destroy() { mDestroyed = true; + onDestroy(); + } + + /** @hide */ + protected void onDestroy() { } /** @hide */ 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/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java index d8afe50d3af3..e7217def7689 100644 --- a/core/java/com/android/internal/jank/InteractionJankMonitor.java +++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java @@ -33,6 +33,7 @@ import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_IN import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_OPEN_ALL_APPS; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_QUICK_SWITCH; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_UNLOCK_ENTRANCE_ANIMATION; +import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_CLOCK_MOVE_ANIMATION; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_LAUNCH_CAMERA; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_OCCLUSION; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_PASSWORD_APPEAR; @@ -231,6 +232,7 @@ public class InteractionJankMonitor { public static final int CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS = 66; public static final int CUJ_LAUNCHER_CLOSE_ALL_APPS_SWIPE = 67; public static final int CUJ_LAUNCHER_CLOSE_ALL_APPS_TO_HOME = 68; + public static final int CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION = 70; private static final int NO_STATSD_LOGGING = -1; @@ -308,6 +310,8 @@ public class InteractionJankMonitor { UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_SWIPE_TO_RECENTS, UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_CLOSE_ALL_APPS_SWIPE, UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_CLOSE_ALL_APPS_TO_HOME, + NO_STATSD_LOGGING, + UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_CLOCK_MOVE_ANIMATION, }; private static volatile InteractionJankMonitor sInstance; @@ -396,7 +400,8 @@ public class InteractionJankMonitor { CUJ_RECENTS_SCROLLING, CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS, CUJ_LAUNCHER_CLOSE_ALL_APPS_SWIPE, - CUJ_LAUNCHER_CLOSE_ALL_APPS_TO_HOME + CUJ_LAUNCHER_CLOSE_ALL_APPS_TO_HOME, + CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION }) @Retention(RetentionPolicy.SOURCE) public @interface CujType { @@ -917,6 +922,8 @@ public class InteractionJankMonitor { return "LAUNCHER_CLOSE_ALL_APPS_SWIPE"; case CUJ_LAUNCHER_CLOSE_ALL_APPS_TO_HOME: return "LAUNCHER_CLOSE_ALL_APPS_TO_HOME"; + case CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION: + return "LOCKSCREEN_CLOCK_MOVE_ANIMATION"; } return "UNKNOWN"; } diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java index bb69192f187f..e603e2ed57f1 100644 --- a/core/java/com/android/internal/policy/PhoneWindow.java +++ b/core/java/com/android/internal/policy/PhoneWindow.java @@ -295,6 +295,7 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { private boolean mClosingActionMenu; private int mVolumeControlStreamType = AudioManager.USE_DEFAULT_STREAM_TYPE; + private int mAudioMode = AudioManager.MODE_NORMAL; private MediaController mMediaController; private AudioManager mAudioManager; @@ -317,6 +318,8 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { } }; + private AudioManager.OnModeChangedListener mOnModeChangedListener; + private Transition mEnterTransition = null; private Transition mReturnTransition = USE_DEFAULT_TRANSITION; private Transition mExitTransition = null; @@ -1950,9 +1953,9 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { case KeyEvent.KEYCODE_VOLUME_UP: case KeyEvent.KEYCODE_VOLUME_DOWN: case KeyEvent.KEYCODE_VOLUME_MUTE: { - // If we have a session send it the volume command, otherwise - // use the suggested stream. - if (mMediaController != null) { + // If we have a session and no active phone call send it the volume command, + // otherwise use the suggested stream. + if (mMediaController != null && !isActivePhoneCallOngoing()) { getMediaSessionManager().dispatchVolumeKeyEventToSessionAsSystemService(event, mMediaController.getSessionToken()); } else { @@ -2003,6 +2006,11 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { return false; } + private boolean isActivePhoneCallOngoing() { + return mAudioMode == AudioManager.MODE_IN_CALL + || mAudioMode == AudioManager.MODE_IN_COMMUNICATION; + } + private KeyguardManager getKeyguardManager() { if (mKeyguardManager == null) { mKeyguardManager = (KeyguardManager) getContext().getSystemService( @@ -2326,6 +2334,14 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { } } + @Override + protected void onDestroy() { + if (mOnModeChangedListener != null) { + getAudioManager().removeOnModeChangedListener(mOnModeChangedListener); + mOnModeChangedListener = null; + } + } + private class PanelMenuPresenterCallback implements MenuPresenter.Callback { @Override public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { @@ -3208,6 +3224,15 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { @Override public void setMediaController(MediaController controller) { mMediaController = controller; + if (controller != null && mOnModeChangedListener == null) { + mAudioMode = getAudioManager().getMode(); + mOnModeChangedListener = mode -> mAudioMode = mode; + getAudioManager().addOnModeChangedListener(getContext().getMainExecutor(), + mOnModeChangedListener); + } else if (mOnModeChangedListener != null) { + getAudioManager().removeOnModeChangedListener(mOnModeChangedListener); + mOnModeChangedListener = null; + } } @Override diff --git a/core/java/com/android/internal/protolog/BaseProtoLogImpl.java b/core/java/com/android/internal/protolog/BaseProtoLogImpl.java index 4d1234f9006a..ac9188a0debc 100644 --- a/core/java/com/android/internal/protolog/BaseProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/BaseProtoLogImpl.java @@ -72,11 +72,13 @@ public class BaseProtoLogImpl { private static final String TAG = "ProtoLog"; private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L; static final String PROTOLOG_VERSION = "1.0.0"; + private static final int DEFAULT_PER_CHUNK_SIZE = 0; private final File mLogFile; private final String mViewerConfigFilename; private final TraceBuffer mBuffer; protected final ProtoLogViewerConfigReader mViewerConfig; + private final int mPerChunkSize; private boolean mProtoLogEnabled; private boolean mProtoLogEnabledLockFree; @@ -156,7 +158,7 @@ public class BaseProtoLogImpl { return; } try { - ProtoOutputStream os = new ProtoOutputStream(); + ProtoOutputStream os = new ProtoOutputStream(mPerChunkSize); long token = os.start(LOG); os.write(MESSAGE_HASH, messageHash); os.write(ELAPSED_REALTIME_NANOS, SystemClock.elapsedRealtimeNanos()); @@ -215,10 +217,16 @@ public class BaseProtoLogImpl { public BaseProtoLogImpl(File file, String viewerConfigFilename, int bufferCapacity, ProtoLogViewerConfigReader viewerConfig) { + this(file, viewerConfigFilename, bufferCapacity, viewerConfig, DEFAULT_PER_CHUNK_SIZE); + } + + public BaseProtoLogImpl(File file, String viewerConfigFilename, int bufferCapacity, + ProtoLogViewerConfigReader viewerConfig, int perChunkSize) { mLogFile = file; mBuffer = new TraceBuffer(bufferCapacity); mViewerConfigFilename = viewerConfigFilename; mViewerConfig = viewerConfig; + mPerChunkSize = perChunkSize; } /** @@ -364,7 +372,7 @@ public class BaseProtoLogImpl { try { long offset = (System.currentTimeMillis() - (SystemClock.elapsedRealtimeNanos() / 1000000)); - ProtoOutputStream proto = new ProtoOutputStream(); + ProtoOutputStream proto = new ProtoOutputStream(mPerChunkSize); proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE); proto.write(VERSION, PROTOLOG_VERSION); proto.write(REAL_TIME_TO_ELAPSED_TIME_OFFSET_MILLIS, offset); diff --git a/core/java/com/android/internal/protolog/ProtoLogImpl.java b/core/java/com/android/internal/protolog/ProtoLogImpl.java index 353c6c083d9d..527cfddf6d8e 100644 --- a/core/java/com/android/internal/protolog/ProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/ProtoLogImpl.java @@ -30,6 +30,7 @@ public class ProtoLogImpl extends BaseProtoLogImpl { private static final int BUFFER_CAPACITY = 1024 * 1024; private static final String LOG_FILENAME = "/data/misc/wmtrace/wm_log.winscope"; private static final String VIEWER_CONFIG_FILENAME = "/system/etc/protolog.conf.json.gz"; + private static final int PER_CHUNK_SIZE = 1024; private static ProtoLogImpl sServiceInstance = null; @@ -94,7 +95,10 @@ public class ProtoLogImpl extends BaseProtoLogImpl { public static synchronized ProtoLogImpl getSingleInstance() { if (sServiceInstance == null) { sServiceInstance = new ProtoLogImpl( - new File(LOG_FILENAME), BUFFER_CAPACITY, new ProtoLogViewerConfigReader()); + new File(LOG_FILENAME) + , BUFFER_CAPACITY + , new ProtoLogViewerConfigReader() + , PER_CHUNK_SIZE); } return sServiceInstance; } @@ -105,8 +109,8 @@ public class ProtoLogImpl extends BaseProtoLogImpl { } public ProtoLogImpl(File logFile, int bufferCapacity, - ProtoLogViewerConfigReader viewConfigReader) { - super(logFile, VIEWER_CONFIG_FILENAME, bufferCapacity, viewConfigReader); - } + ProtoLogViewerConfigReader viewConfigReader, int perChunkSize) { + super(logFile, VIEWER_CONFIG_FILENAME, bufferCapacity, viewConfigReader, perChunkSize); + } } 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/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 1cf819af7a24..3d7fb16bb846 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -1051,6 +1051,12 @@ "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/WindowContainer.java" }, + "-1104347731": { + "message": "Setting requested orientation %s for %s", + "level": "VERBOSE", + "group": "WM_DEBUG_ORIENTATION", + "at": "com\/android\/server\/wm\/ActivityRecord.java" + }, "-1103716954": { "message": "Not removing %s due to exit animation", "level": "VERBOSE", diff --git a/libs/WindowManager/Shell/res/color/split_divider_background.xml b/libs/WindowManager/Shell/res/color-night/taskbar_background.xml index 049980803ee3..9473cdd607d6 100644 --- a/libs/WindowManager/Shell/res/color/split_divider_background.xml +++ b/libs/WindowManager/Shell/res/color-night/taskbar_background.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ 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. @@ -14,6 +14,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> +<!-- Should be the same as in packages/apps/Launcher3/res/color-night-v31/taskbar_background.xml --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:color="@android:color/system_neutral1_500" android:lStar="15" /> </selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/color/taskbar_background.xml b/libs/WindowManager/Shell/res/color/taskbar_background.xml index b3d260299106..0e165fca4fd3 100644 --- a/libs/WindowManager/Shell/res/color/taskbar_background.xml +++ b/libs/WindowManager/Shell/res/color/taskbar_background.xml @@ -16,5 +16,5 @@ --> <!-- Should be the same as in packages/apps/Launcher3/res/color-v31/taskbar_background.xml --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:color="@android:color/system_neutral1_500" android:lStar="15" /> + <item android:color="@android:color/system_neutral1_500" android:lStar="95" /> </selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values-night/colors.xml b/libs/WindowManager/Shell/res/values-night/colors.xml index 83c4d93982f4..5c6bb57a7f1c 100644 --- a/libs/WindowManager/Shell/res/values-night/colors.xml +++ b/libs/WindowManager/Shell/res/values-night/colors.xml @@ -15,6 +15,7 @@ --> <resources> + <color name="docked_divider_handle">#ffffff</color> <!-- Bubbles --> <color name="bubbles_icon_tint">@color/GM2_grey_200</color> <!-- Splash screen--> diff --git a/libs/WindowManager/Shell/res/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml index 965ab1519df4..6fb70006e67f 100644 --- a/libs/WindowManager/Shell/res/values/colors.xml +++ b/libs/WindowManager/Shell/res/values/colors.xml @@ -17,7 +17,8 @@ */ --> <resources> - <color name="docked_divider_handle">#ffffff</color> + <color name="docked_divider_handle">#000000</color> + <color name="split_divider_background">@color/taskbar_background</color> <drawable name="forced_resizable_background">#59000000</drawable> <color name="minimize_dock_shadow_start">#60000000</color> <color name="minimize_dock_shadow_end">#00000000</color> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 71e15c12b9c0..541c0f04b9b9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -61,6 +61,7 @@ import android.os.Binder; import android.os.Handler; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.service.notification.NotificationListenerService; @@ -125,6 +126,39 @@ public class BubbleController implements ConfigurationChangeListener { private static final String SYSTEM_DIALOG_REASON_KEY = "reason"; private static final String SYSTEM_DIALOG_REASON_GESTURE_NAV = "gestureNav"; + // TODO(b/256873975) Should use proper flag when available to shell/launcher + /** + * Whether bubbles are showing in the bubble bar from launcher. This is only available + * on large screens and {@link BubbleController#isShowingAsBubbleBar()} should be used + * to check all conditions that indicate if the bubble bar is in use. + */ + private static final boolean BUBBLE_BAR_ENABLED = + SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false); + + + /** + * Common interface to send updates to bubble views. + */ + public interface BubbleViewCallback { + /** Called when the provided bubble should be removed. */ + void removeBubble(Bubble removedBubble); + /** Called when the provided bubble should be added. */ + void addBubble(Bubble addedBubble); + /** Called when the provided bubble should be updated. */ + void updateBubble(Bubble updatedBubble); + /** Called when the provided bubble should be selected. */ + void selectionChanged(BubbleViewProvider selectedBubble); + /** Called when the provided bubble's suppression state has changed. */ + void suppressionChanged(Bubble bubble, boolean isSuppressed); + /** Called when the expansion state of bubbles has changed. */ + void expansionChanged(boolean isExpanded); + /** + * Called when the order of the bubble list has changed. Depending on the expanded state + * the pointer might need to be updated. + */ + void bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer); + } + private final Context mContext; private final BubblesImpl mImpl = new BubblesImpl(); private Bubbles.BubbleExpandListener mExpandListener; @@ -147,12 +181,8 @@ public class BubbleController implements ConfigurationChangeListener { // Used to post to main UI thread private final ShellExecutor mMainExecutor; private final Handler mMainHandler; - private final ShellExecutor mBackgroundExecutor; - // Whether or not we should show bubbles pinned at the bottom of the screen. - private boolean mIsBubbleBarEnabled; - private BubbleLogger mLogger; private BubbleData mBubbleData; @Nullable private BubbleStackView mStackView; @@ -533,10 +563,10 @@ public class BubbleController implements ConfigurationChangeListener { mDataRepository.removeBubblesForUser(removedUserId, parentUserId); } - // TODO(b/256873975): Should pass this into the constructor once flags are available to shell. - /** Sets whether the bubble bar is enabled (i.e. bubbles pinned to bottom on large screens). */ - public void setBubbleBarEnabled(boolean enabled) { - mIsBubbleBarEnabled = enabled; + /** Whether bubbles are showing in the bubble bar. */ + public boolean isShowingAsBubbleBar() { + // TODO(b/269670598): should also check that we're in gesture nav + return BUBBLE_BAR_ENABLED && mBubblePositioner.isLargeScreen(); } /** Whether this userId belongs to the current user. */ @@ -605,12 +635,6 @@ public class BubbleController implements ConfigurationChangeListener { mStackView.setUnbubbleConversationCallback(mSysuiProxy::onUnbubbleConversation); } - if (mIsBubbleBarEnabled && mBubblePositioner.isLargeScreen()) { - mBubblePositioner.setUsePinnedLocation(true); - } else { - mBubblePositioner.setUsePinnedLocation(false); - } - addToWindowManagerMaybe(); } @@ -1284,6 +1308,58 @@ public class BubbleController implements ConfigurationChangeListener { }); } + private final BubbleViewCallback mBubbleViewCallback = new BubbleViewCallback() { + @Override + public void removeBubble(Bubble removedBubble) { + if (mStackView != null) { + mStackView.removeBubble(removedBubble); + } + } + + @Override + public void addBubble(Bubble addedBubble) { + if (mStackView != null) { + mStackView.addBubble(addedBubble); + } + } + + @Override + public void updateBubble(Bubble updatedBubble) { + if (mStackView != null) { + mStackView.updateBubble(updatedBubble); + } + } + + @Override + public void bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer) { + if (mStackView != null) { + mStackView.updateBubbleOrder(bubbleOrder, updatePointer); + } + } + + @Override + public void suppressionChanged(Bubble bubble, boolean isSuppressed) { + if (mStackView != null) { + mStackView.setBubbleSuppressed(bubble, isSuppressed); + } + } + + @Override + public void expansionChanged(boolean isExpanded) { + if (mStackView != null) { + mStackView.setExpanded(isExpanded); + } + } + + @Override + public void selectionChanged(BubbleViewProvider selectedBubble) { + if (mStackView != null) { + mStackView.setSelectedBubble(selectedBubble); + } + + } + }; + @SuppressWarnings("FieldCanBeLocal") private final BubbleData.Listener mBubbleDataListener = new BubbleData.Listener() { @@ -1306,7 +1382,8 @@ public class BubbleController implements ConfigurationChangeListener { // Lazy load overflow bubbles from disk loadOverflowBubblesFromDisk(); - mStackView.updateOverflowButtonDot(); + // If bubbles in the overflow have a dot, make sure the overflow shows a dot + updateOverflowButtonDot(); // Update bubbles in overflow. if (mOverflowListener != null) { @@ -1321,9 +1398,7 @@ public class BubbleController implements ConfigurationChangeListener { final Bubble bubble = removed.first; @Bubbles.DismissReason final int reason = removed.second; - if (mStackView != null) { - mStackView.removeBubble(bubble); - } + mBubbleViewCallback.removeBubble(bubble); // Leave the notification in place if we're dismissing due to user switching, or // because DND is suppressing the bubble. In both of those cases, we need to be able @@ -1353,49 +1428,47 @@ public class BubbleController implements ConfigurationChangeListener { } mDataRepository.removeBubbles(mCurrentUserId, bubblesToBeRemovedFromRepository); - if (update.addedBubble != null && mStackView != null) { + if (update.addedBubble != null) { mDataRepository.addBubble(mCurrentUserId, update.addedBubble); - mStackView.addBubble(update.addedBubble); + mBubbleViewCallback.addBubble(update.addedBubble); } - if (update.updatedBubble != null && mStackView != null) { - mStackView.updateBubble(update.updatedBubble); + if (update.updatedBubble != null) { + mBubbleViewCallback.updateBubble(update.updatedBubble); } - if (update.suppressedBubble != null && mStackView != null) { - mStackView.setBubbleSuppressed(update.suppressedBubble, true); + if (update.suppressedBubble != null) { + mBubbleViewCallback.suppressionChanged(update.suppressedBubble, true); } - if (update.unsuppressedBubble != null && mStackView != null) { - mStackView.setBubbleSuppressed(update.unsuppressedBubble, false); + if (update.unsuppressedBubble != null) { + mBubbleViewCallback.suppressionChanged(update.unsuppressedBubble, false); } boolean collapseStack = update.expandedChanged && !update.expanded; // At this point, the correct bubbles are inflated in the stack. // Make sure the order in bubble data is reflected in bubble row. - if (update.orderChanged && mStackView != null) { + if (update.orderChanged) { mDataRepository.addBubbles(mCurrentUserId, update.bubbles); // if the stack is going to be collapsed, do not update pointer position // after reordering - mStackView.updateBubbleOrder(update.bubbles, !collapseStack); + mBubbleViewCallback.bubbleOrderChanged(update.bubbles, !collapseStack); } if (collapseStack) { - mStackView.setExpanded(false); + mBubbleViewCallback.expansionChanged(/* expanded= */ false); mSysuiProxy.requestNotificationShadeTopUi(false, TAG); } - if (update.selectionChanged && mStackView != null) { - mStackView.setSelectedBubble(update.selectedBubble); + if (update.selectionChanged) { + mBubbleViewCallback.selectionChanged(update.selectedBubble); } // Expanding? Apply this last. if (update.expandedChanged && update.expanded) { - if (mStackView != null) { - mStackView.setExpanded(true); - mSysuiProxy.requestNotificationShadeTopUi(true, TAG); - } + mBubbleViewCallback.expansionChanged(/* expanded= */ true); + mSysuiProxy.requestNotificationShadeTopUi(true, TAG); } mSysuiProxy.notifyInvalidateNotifications("BubbleData.Listener.applyUpdate"); @@ -1406,6 +1479,19 @@ public class BubbleController implements ConfigurationChangeListener { } }; + private void updateOverflowButtonDot() { + BubbleOverflow overflow = mBubbleData.getOverflow(); + if (overflow == null) return; + + for (Bubble b : mBubbleData.getOverflowBubbles()) { + if (b.showDot()) { + overflow.setShowDot(true); + return; + } + } + overflow.setShowDot(false); + } + private boolean handleDismissalInterception(BubbleEntry entry, @Nullable List<BubbleEntry> children, IntConsumer removeCallback) { if (isSummaryOfBubbles(entry)) { @@ -1852,13 +1938,6 @@ public class BubbleController implements ConfigurationChangeListener { } @Override - public void setBubbleBarEnabled(boolean enabled) { - mMainExecutor.execute(() -> { - BubbleController.this.setBubbleBarEnabled(enabled); - }); - } - - @Override public void onNotificationPanelExpandedChanged(boolean expanded) { mMainExecutor.execute( () -> BubbleController.this.onNotificationPanelExpandedChanged(expanded)); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index 6230d22ebe12..3fd09675a245 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -283,7 +283,7 @@ public class BubbleData { } boolean isShowingOverflow() { - return mShowingOverflow && (isExpanded() || mPositioner.showingInTaskbar()); + return mShowingOverflow && isExpanded(); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index 07c58527a815..5ea2450114f0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -18,9 +18,6 @@ package com.android.wm.shell.bubbles; import static android.view.View.LAYOUT_DIRECTION_RTL; -import static java.lang.annotation.RetentionPolicy.SOURCE; - -import android.annotation.IntDef; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; @@ -39,8 +36,6 @@ import androidx.annotation.VisibleForTesting; import com.android.launcher3.icons.IconNormalizer; import com.android.wm.shell.R; -import java.lang.annotation.Retention; - /** * Keeps track of display size, configuration, and specific bubble sizes. One place for all * placement and positioning calculations to refer to. @@ -50,15 +45,6 @@ public class BubblePositioner { ? "BubblePositioner" : BubbleDebugConfig.TAG_BUBBLES; - @Retention(SOURCE) - @IntDef({TASKBAR_POSITION_NONE, TASKBAR_POSITION_RIGHT, TASKBAR_POSITION_LEFT, - TASKBAR_POSITION_BOTTOM}) - @interface TaskbarPosition {} - public static final int TASKBAR_POSITION_NONE = -1; - public static final int TASKBAR_POSITION_RIGHT = 0; - public static final int TASKBAR_POSITION_LEFT = 1; - public static final int TASKBAR_POSITION_BOTTOM = 2; - /** When the bubbles are collapsed in a stack only some of them are shown, this is how many. **/ public static final int NUM_VISIBLE_WHEN_RESTING = 2; /** Indicates a bubble's height should be the maximum available space. **/ @@ -108,15 +94,9 @@ public class BubblePositioner { private int mOverflowHeight; private int mMinimumFlyoutWidthLargeScreen; - private PointF mPinLocation; private PointF mRestingStackPosition; private int[] mPaddings = new int[4]; - private boolean mShowingInTaskbar; - private @TaskbarPosition int mTaskbarPosition = TASKBAR_POSITION_NONE; - private int mTaskbarIconSize; - private int mTaskbarSize; - public BubblePositioner(Context context, WindowManager windowManager) { mContext = context; mWindowManager = windowManager; @@ -153,27 +133,11 @@ public class BubblePositioner { + " insets: " + insets + " isLargeScreen: " + mIsLargeScreen + " isSmallTablet: " + mIsSmallTablet - + " bounds: " + bounds - + " showingInTaskbar: " + mShowingInTaskbar); + + " bounds: " + bounds); } updateInternal(mRotation, insets, bounds); } - /** - * Updates position information to account for taskbar state. - * - * @param taskbarPosition which position the taskbar is displayed in. - * @param showingInTaskbar whether the taskbar is being shown. - */ - public void updateForTaskbar(int iconSize, - @TaskbarPosition int taskbarPosition, boolean showingInTaskbar, int taskbarSize) { - mShowingInTaskbar = showingInTaskbar; - mTaskbarIconSize = iconSize; - mTaskbarPosition = taskbarPosition; - mTaskbarSize = taskbarSize; - update(); - } - @VisibleForTesting public void updateInternal(int rotation, Insets insets, Rect bounds) { mRotation = rotation; @@ -232,10 +196,6 @@ public class BubblePositioner { R.dimen.bubbles_flyout_min_width_large_screen); mMaxBubbles = calculateMaxBubbles(); - - if (mShowingInTaskbar) { - adjustForTaskbar(); - } } /** @@ -260,30 +220,6 @@ public class BubblePositioner { return mDefaultMaxBubbles; } - /** - * Taskbar insets appear as navigationBar insets, however, unlike navigationBar this should - * not inset bubbles UI as bubbles floats above the taskbar. This adjust the available space - * and insets to account for the taskbar. - */ - // TODO(b/171559950): When the insets are reported correctly we can remove this logic - private void adjustForTaskbar() { - // When bar is showing on edges... subtract that inset because we appear on top - if (mShowingInTaskbar && mTaskbarPosition != TASKBAR_POSITION_BOTTOM) { - WindowInsets metricInsets = mWindowManager.getCurrentWindowMetrics().getWindowInsets(); - Insets navBarInsets = metricInsets.getInsetsIgnoringVisibility( - WindowInsets.Type.navigationBars()); - int newInsetLeft = mInsets.left; - int newInsetRight = mInsets.right; - if (mTaskbarPosition == TASKBAR_POSITION_LEFT) { - mPositionRect.left -= navBarInsets.left; - newInsetLeft -= navBarInsets.left; - } else if (mTaskbarPosition == TASKBAR_POSITION_RIGHT) { - mPositionRect.right += navBarInsets.right; - newInsetRight -= navBarInsets.right; - } - mInsets = Insets.of(newInsetLeft, mInsets.top, newInsetRight, mInsets.bottom); - } - } /** * @return a rect of available screen space accounting for orientation, system bars and cutouts. @@ -327,14 +263,12 @@ public class BubblePositioner { * to the left or right side. */ public boolean showBubblesVertically() { - return isLandscape() || mShowingInTaskbar || mIsLargeScreen; + return isLandscape() || mIsLargeScreen; } /** Size of the bubble. */ public int getBubbleSize() { - return (mShowingInTaskbar && mTaskbarIconSize > 0) - ? mTaskbarIconSize - : mBubbleSize; + return mBubbleSize; } /** The amount of padding at the top of the screen that the bubbles avoid when being placed. */ @@ -699,9 +633,6 @@ public class BubblePositioner { /** The position the bubble stack should rest at when collapsed. */ public PointF getRestingPosition() { - if (mPinLocation != null) { - return mPinLocation; - } if (mRestingStackPosition == null) { return getDefaultStartPosition(); } @@ -713,9 +644,6 @@ public class BubblePositioner { * is being shown. */ public PointF getDefaultStartPosition() { - if (mPinLocation != null) { - return mPinLocation; - } // Start on the left if we're in LTR, right otherwise. final boolean startOnLeft = mContext.getResources().getConfiguration().getLayoutDirection() @@ -730,7 +658,6 @@ public class BubblePositioner { 1 /* default starts with 1 bubble */)); } - /** * Returns the region that the stack position must stay within. This goes slightly off the left * and right sides of the screen, below the status bar/cutout and above the navigation bar. @@ -751,39 +678,6 @@ public class BubblePositioner { } /** - * @return whether the bubble stack is pinned to the taskbar. - */ - public boolean showingInTaskbar() { - return mShowingInTaskbar; - } - - /** - * @return the taskbar position if set. - */ - public int getTaskbarPosition() { - return mTaskbarPosition; - } - - public int getTaskbarSize() { - return mTaskbarSize; - } - - /** - * In some situations bubbles will be pinned to a specific onscreen location. This sets whether - * bubbles should be pinned or not. - */ - public void setUsePinnedLocation(boolean usePinnedLocation) { - if (usePinnedLocation) { - mShowingInTaskbar = true; - mPinLocation = new PointF(mPositionRect.right - mBubbleSize, - mPositionRect.bottom - mBubbleSize); - } else { - mPinLocation = null; - mShowingInTaskbar = false; - } - } - - /** * Navigation bar has an area where system gestures can be started from. * * @return {@link Rect} for system navigation bar gesture zone diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index f2afefe243bc..5ecbd6b596b6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -680,8 +680,6 @@ public class BubbleStackView extends FrameLayout // Re-show the expanded view if we hid it. showExpandedViewIfNeeded(); - } else if (mPositioner.showingInTaskbar()) { - mStackAnimationController.snapStackBack(); } else { // Fling the stack to the edge, and save whether or not it's going to end up on // the left side of the screen. @@ -1362,16 +1360,6 @@ public class BubbleStackView extends FrameLayout updateOverflowVisibility(); } - void updateOverflowButtonDot() { - for (Bubble b : mBubbleData.getOverflowBubbles()) { - if (b.showDot()) { - mBubbleOverflow.setShowDot(true); - return; - } - } - mBubbleOverflow.setShowDot(false); - } - /** * Handle theme changes. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index df4325763a17..a5deac5a51da 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -257,11 +257,6 @@ public interface Bubbles { */ void onUserRemoved(int removedUserId); - /** - * Sets whether bubble bar should be enabled or not. - */ - void setBubbleBarEnabled(boolean enabled); - /** Listener to find out about stack expansion / collapse events. */ interface BubbleExpandListener { /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java index 0ee0ea60a1bc..5533842f2d89 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java @@ -417,23 +417,9 @@ public class StackAnimationController extends } /** - * Snaps the stack back to the previous resting position. - */ - public void snapStackBack() { - if (mLayout == null) { - return; - } - PointF p = getStackPositionAlongNearestHorizontalEdge(); - springStackAfterFling(p.x, p.y); - } - - /** * Where the stack would be if it were snapped to the nearest horizontal edge (left or right). */ public PointF getStackPositionAlongNearestHorizontalEdge() { - if (mPositioner.showingInTaskbar()) { - return mPositioner.getRestingPosition(); - } final PointF stackPos = getStackPosition(); final boolean onLeft = mLayout.isFirstChildXLeftOfCenter(stackPos.x); final RectF bounds = mPositioner.getAllowableStackPositionRegion(getBubbleCount()); 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/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index f616e6f64750..ffc56b6f6106 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -120,6 +120,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange private int mOrientation; private int mRotation; private int mDensity; + private int mUiMode; private final boolean mDimNonImeSide; private ValueAnimator mDividerFlingAnimator; @@ -295,10 +296,12 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange final Rect rootBounds = configuration.windowConfiguration.getBounds(); final int orientation = configuration.orientation; final int density = configuration.densityDpi; + final int uiMode = configuration.uiMode; if (mOrientation == orientation && mRotation == rotation && mDensity == density + && mUiMode == uiMode && mRootBounds.equals(rootBounds)) { return false; } @@ -310,6 +313,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mRootBounds.set(rootBounds); mRotation = rotation; mDensity = density; + mUiMode = uiMode; mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds, null); updateDividerConfig(mContext); initDividerPosition(mTempRect); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 72dc771ee08c..ef21c7e9ec0c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -51,6 +51,7 @@ import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; +import com.android.wm.shell.common.TabletopModeController; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ShellAnimationThread; @@ -171,6 +172,18 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides + static TabletopModeController provideTabletopModeController( + Context context, + ShellInit shellInit, + DevicePostureController postureController, + DisplayController displayController, + @ShellMainThread ShellExecutor mainExecutor) { + return new TabletopModeController( + context, shellInit, postureController, displayController, mainExecutor); + } + + @WMSingleton + @Provides static DragAndDropController provideDragAndDropController(Context context, ShellInit shellInit, ShellController shellController, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java index ac13f96585b6..f9e0ca53b32d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java @@ -247,6 +247,11 @@ public class KidsModeTaskOrganizer extends ShellTaskOrganizer { mLaunchRootTask = taskInfo; } + if (mHomeTask != null && mHomeTask.taskId == taskInfo.taskId + && !taskInfo.equals(mHomeTask)) { + mHomeTask = taskInfo; + } + super.onTaskInfoChanged(taskInfo); } @@ -364,6 +369,7 @@ public class KidsModeTaskOrganizer extends ShellTaskOrganizer { final WindowContainerTransaction wct = getWindowContainerTransaction(); final Rect taskBounds = calculateBounds(); wct.setBounds(mLaunchRootTask.token, taskBounds); + wct.setBounds(mHomeTask.token, new Rect(0, 0, mDisplayWidth, mDisplayHeight)); mSyncQueue.queue(wct); final SurfaceControl finalLeash = mLaunchRootLeash; mSyncQueue.runInSync(t -> { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl index 2624ee536b58..d961d8658b98 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl @@ -70,4 +70,9 @@ interface IPip { * Sets the next pip animation type to be the alpha animation. */ oneway void setPipAnimationTypeToAlpha() = 5; + + /** + * Sets the height and visibility of the Launcher keep clear area. + */ + oneway void setLauncherKeepClearAreaHeight(boolean visible, int height) = 6; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java index 480bf93b2ddb..53bf42a3c911 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java @@ -39,6 +39,9 @@ import android.window.TaskSnapshot; * Represents the content overlay used during the entering PiP animation. */ public abstract class PipContentOverlay { + // Fixed string used in WMShellFlickerTests + protected static final String LAYER_NAME = "PipContentOverlay"; + protected SurfaceControl mLeash; /** Attaches the internal {@link #mLeash} to the given parent leash. */ @@ -86,7 +89,7 @@ public abstract class PipContentOverlay { mContext = context; mLeash = new SurfaceControl.Builder(new SurfaceSession()) .setCallsite(TAG) - .setName(TAG) + .setName(LAYER_NAME) .setColorLayer() .build(); } @@ -139,7 +142,7 @@ public abstract class PipContentOverlay { mSourceRectHint = new Rect(sourceRectHint); mLeash = new SurfaceControl.Builder(new SurfaceSession()) .setCallsite(TAG) - .setName(TAG) + .setName(LAYER_NAME) .build(); } @@ -194,7 +197,7 @@ public abstract class PipContentOverlay { prepareAppIconOverlay(activityInfo); mLeash = new SurfaceControl.Builder(new SurfaceSession()) .setCallsite(TAG) - .setName(TAG) + .setName(LAYER_NAME) .build(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index f11836ea5bee..eb336d56b62c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -1179,6 +1179,20 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } /** + * Directly update the animator bounds. + */ + public void updateAnimatorBounds(Rect bounds) { + final PipAnimationController.PipTransitionAnimator animator = + mPipAnimationController.getCurrentAnimator(); + if (animator != null && animator.isRunning()) { + if (animator.getAnimationType() == ANIM_TYPE_BOUNDS) { + animator.updateEndValue(bounds); + } + animator.setDestinationBounds(bounds); + } + } + + /** * Handles all changes to the PictureInPictureParams. */ protected void applyNewPictureInPictureParams(@NonNull PictureInPictureParams params) { @@ -1594,7 +1608,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, // source rect hint to enter PiP use bounds animation. if (sourceHintRect == null) { if (SystemProperties.getBoolean( - "persist.wm.debug.enable_pip_app_icon_overlay", false)) { + "persist.wm.debug.enable_pip_app_icon_overlay", true)) { animator.setAppIconContentOverlay( mContext, currentBounds, mTaskInfo.topActivityInfo); } else { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index e5c0570841f4..7234b15bf6d2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -804,7 +804,7 @@ public class PipTransition extends PipTransitionController { // We use content overlay when there is no source rect hint to enter PiP use bounds // animation. if (SystemProperties.getBoolean( - "persist.wm.debug.enable_pip_app_icon_overlay", false)) { + "persist.wm.debug.enable_pip_app_icon_overlay", true)) { animator.setAppIconContentOverlay( mContext, currentBounds, taskInfo.topActivityInfo); } else { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java index c6b5ce93fd35..db6138a0891f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java @@ -93,6 +93,11 @@ public class PipTransitionState { return hasEnteredPip(mState); } + /** Returns true if activity is currently entering PiP mode. */ + public boolean isEnteringPip() { + return isEnteringPip(mState); + } + public void setInSwipePipToHomeTransition(boolean inSwipePipToHomeTransition) { mInSwipePipToHomeTransition = inSwipePipToHomeTransition; } @@ -130,6 +135,11 @@ public class PipTransitionState { return state == ENTERED_PIP; } + /** Returns true if activity is currently entering PiP mode. */ + public static boolean isEnteringPip(@TransitionState int state) { + return state == ENTERING_PIP; + } + public interface OnPipTransitionStateChangedListener { void onPipTransitionStateChanged(@TransitionState int oldState, @TransitionState int newState); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index fa3efeb51bd0..0d5f1432d204 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -101,6 +101,7 @@ import com.android.wm.shell.sysui.UserChangeListener; import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; +import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -181,14 +182,20 @@ public class PipController implements PipTransitionController.PipTransitionCallb // early bail out if the keep clear areas feature is disabled return; } - // only move if already in pip, other transitions account for keep clear areas - if (mPipTransitionState.hasEnteredPip()) { + // only move if we're in PiP or transitioning into PiP + if (!mPipTransitionState.shouldBlockResizeRequest()) { Rect destBounds = mPipKeepClearAlgorithm.adjust(mPipBoundsState, mPipBoundsAlgorithm); // only move if the bounds are actually different if (destBounds != mPipBoundsState.getBounds()) { - mPipTaskOrganizer.scheduleAnimateResizePip(destBounds, - mEnterAnimationDuration, null); + if (mPipTransitionState.hasEnteredPip()) { + // if already in PiP, schedule separate animation + mPipTaskOrganizer.scheduleAnimateResizePip(destBounds, + mEnterAnimationDuration, null); + } else if (mPipTransitionState.isEnteringPip()) { + // while entering PiP we just need to update animator bounds + mPipTaskOrganizer.updateAnimatorBounds(destBounds); + } } } } @@ -874,6 +881,21 @@ public class PipController implements PipTransitionController.PipTransitionCallb } } + private void setLauncherKeepClearAreaHeight(boolean visible, int height) { + if (visible) { + Rect rect = new Rect( + 0, mPipBoundsState.getDisplayBounds().bottom - height, + mPipBoundsState.getDisplayBounds().right, + mPipBoundsState.getDisplayBounds().bottom); + Set<Rect> restrictedKeepClearAreas = new HashSet<>( + mPipBoundsState.getRestrictedKeepClearAreas()); + restrictedKeepClearAreas.add(rect); + mPipBoundsState.setKeepClearAreas(restrictedKeepClearAreas, + mPipBoundsState.getUnrestrictedKeepClearAreas()); + updatePipPositionForKeepClearAreas(); + } + } + private void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) { mOnIsInPipStateChangedListener = callback; if (mOnIsInPipStateChangedListener != null) { @@ -1237,6 +1259,14 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override + public void setLauncherKeepClearAreaHeight(boolean visible, int height) { + executeRemoteCallWithTaskPermission(mController, "setLauncherKeepClearAreaHeight", + (controller) -> { + controller.setLauncherKeepClearAreaHeight(visible, height); + }); + } + + @Override public void setPipAnimationListener(IPipAnimationListener listener) { executeRemoteCallWithTaskPermission(mController, "setPipAnimationListener", (controller) -> { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java index 75f9a4c33af9..c9b3a1af6507 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java @@ -50,6 +50,8 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { Consts.TAG_WM_SHELL), WM_SHELL_FLOATING_APPS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM_SHELL), + WM_SHELL_FOLDABLE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, + Consts.TAG_WM_SHELL), TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest"); private final boolean mEnabled; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 7d3e7ca671e5..71ee690146f9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -207,6 +207,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private boolean mIsDividerRemoteAnimating; private boolean mIsDropEntering; private boolean mIsExiting; + private boolean mIsRootTranslucent; private DefaultMixedHandler mMixedHandler; private final Toast mSplitUnsupportedToast; @@ -422,6 +423,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } + if (!isSplitActive()) { + // prevent the fling divider to center transitioni if split screen didn't active. + mIsDropEntering = true; + } + setSideStagePosition(sideStagePosition, wct); final WindowContainerTransaction evictWct = new WindowContainerTransaction(); targetStage.evictAllChildren(evictWct); @@ -436,28 +442,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // reparent the task to an invisible split root will make the activity invisible. Reorder // the root task to front to make the entering transition from pip to split smooth. wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, true); wct.reorder(targetStage.mRootTaskInfo.token, true); - wct.setForceTranslucent(targetStage.mRootTaskInfo.token, true); - // prevent the fling divider to center transition - mIsDropEntering = true; - targetStage.addTask(task, wct); - if (ENABLE_SHELL_TRANSITIONS) { - prepareEnterSplitScreen(wct); - mSplitTransitions.startEnterTransition(TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct, - null, this, null /* consumedCallback */, (finishWct, finishT) -> { - if (!evictWct.isEmpty()) { - finishWct.merge(evictWct, true); - } - } /* finishedCallback */); - } else { - if (!evictWct.isEmpty()) { - wct.merge(evictWct, true /* transfer */); - } - mTaskOrganizer.applyTransaction(wct); + if (!evictWct.isEmpty()) { + wct.merge(evictWct, true /* transfer */); } + mTaskOrganizer.applyTransaction(wct); return true; } @@ -716,7 +707,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitLayout.setDivideRatio(splitRatio); updateWindowBounds(mSplitLayout, wct); wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, false); + setRootForceTranslucent(false, wct); // Make sure the launch options will put tasks in the corresponding split roots mainOptions = mainOptions != null ? mainOptions : new Bundle(); @@ -764,17 +755,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 +780,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 +801,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 +812,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 +911,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); + setRootForceTranslucent(false, wct); // 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 +986,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 +1060,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. @@ -1247,7 +1335,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSideStage.removeAllTasks(wct, false /* toTop */); mMainStage.deactivate(wct, false /* toTop */); wct.reorder(mRootTaskInfo.token, false /* onTop */); - wct.setForceTranslucent(mRootTaskInfo.token, true); + setRootForceTranslucent(true, wct); wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); onTransitionAnimationComplete(); } else { @@ -1279,7 +1367,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainStage.deactivate(finishedWCT, childrenToTop == mMainStage /* toTop */); mSideStage.removeAllTasks(finishedWCT, childrenToTop == mSideStage /* toTop */); finishedWCT.reorder(mRootTaskInfo.token, false /* toTop */); - finishedWCT.setForceTranslucent(mRootTaskInfo.token, true); + setRootForceTranslucent(true, wct); finishedWCT.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); mSyncQueue.queue(finishedWCT); mSyncQueue.runInSync(at -> { @@ -1391,7 +1479,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainStage.activate(wct, true /* includingTopTask */); updateWindowBounds(mSplitLayout, wct); wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, false); + setRootForceTranslucent(false, wct); } void finishEnterSplitScreen(SurfaceControl.Transaction t) { @@ -1595,6 +1683,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mRootTaskInfo = null; mRootTaskLeash = null; + mIsRootTranslucent = false; } @@ -1613,7 +1702,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Make the stages adjacent to each other so they occlude what's behind them. wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token); wct.setLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token); - wct.setForceTranslucent(mRootTaskInfo.token, true); + setRootForceTranslucent(true, wct); mSplitLayout.getInvisibleBounds(mTempRect1); wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); mSyncQueue.queue(wct); @@ -1637,7 +1726,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSideStage.evictOtherChildren(wct, taskId); updateWindowBounds(mSplitLayout, wct); wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, false); + setRootForceTranslucent(false, wct); mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> { @@ -1661,6 +1750,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mDisplayInsetsController.removeInsetsChangedListener(mDisplayId, mSplitLayout); } + private void setRootForceTranslucent(boolean translucent, WindowContainerTransaction wct) { + if (mIsRootTranslucent == translucent) return; + + mIsRootTranslucent = translucent; + wct.setForceTranslucent(mRootTaskInfo.token, translucent); + } + private void onStageVisibilityChanged(StageListenerImpl stageListener) { // If split didn't active, just ignore this callback because we should already did these // on #applyExitSplitScreen. @@ -1687,10 +1783,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Split entering background. wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, true /* setReparentLeafTaskIfRelaunch */); - wct.setForceTranslucent(mRootTaskInfo.token, true); + setRootForceTranslucent(true, wct); } else { wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, false /* setReparentLeafTaskIfRelaunch */); + setRootForceTranslucent(false, wct); } mSyncQueue.queue(wct); @@ -1822,7 +1919,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainStage.activate(wct, true /* includingTopTask */); updateWindowBounds(mSplitLayout, wct); wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, false); + setRootForceTranslucent(false, wct); } mSyncQueue.queue(wct); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java index a3d364a0068e..0bce3acecb3c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java @@ -40,6 +40,7 @@ class TaskPositioner implements DragPositioningCallback { private final DisplayController mDisplayController; private final WindowDecoration mWindowDecoration; + private final Rect mTempBounds = new Rect(); private final Rect mTaskBoundsAtDragStart = new Rect(); private final PointF mRepositionStartPoint = new PointF(); private final Rect mRepositionTaskBounds = new Rect(); @@ -117,17 +118,32 @@ class TaskPositioner implements DragPositioningCallback { final float deltaX = x - mRepositionStartPoint.x; final float deltaY = y - mRepositionStartPoint.y; mRepositionTaskBounds.set(mTaskBoundsAtDragStart); + + final Rect stableBounds = mTempBounds; + // Make sure the new resizing destination in any direction falls within the stable bounds. + // If not, set the bounds back to the old location that was valid to avoid conflicts with + // some regions such as the gesture area. + mDisplayController.getDisplayLayout(mWindowDecoration.mDisplay.getDisplayId()) + .getStableBounds(stableBounds); if ((mCtrlType & CTRL_TYPE_LEFT) != 0) { - mRepositionTaskBounds.left += deltaX; + final int candidateLeft = mRepositionTaskBounds.left + (int) deltaX; + mRepositionTaskBounds.left = (candidateLeft > stableBounds.left) + ? candidateLeft : oldLeft; } if ((mCtrlType & CTRL_TYPE_RIGHT) != 0) { - mRepositionTaskBounds.right += deltaX; + final int candidateRight = mRepositionTaskBounds.right + (int) deltaX; + mRepositionTaskBounds.right = (candidateRight < stableBounds.right) + ? candidateRight : oldRight; } if ((mCtrlType & CTRL_TYPE_TOP) != 0) { - mRepositionTaskBounds.top += deltaY; + final int candidateTop = mRepositionTaskBounds.top + (int) deltaY; + mRepositionTaskBounds.top = (candidateTop > stableBounds.top) + ? candidateTop : oldTop; } if ((mCtrlType & CTRL_TYPE_BOTTOM) != 0) { - mRepositionTaskBounds.bottom += deltaY; + final int candidateBottom = mRepositionTaskBounds.bottom + (int) deltaY; + mRepositionTaskBounds.bottom = (candidateBottom < stableBounds.bottom) + ? candidateBottom : oldBottom; } if (mCtrlType == CTRL_TYPE_UNDEFINED) { mRepositionTaskBounds.offset((int) deltaX, (int) deltaY); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TabletopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TabletopModeControllerTest.java new file mode 100644 index 000000000000..96d202ce3a85 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TabletopModeControllerTest.java @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_CLOSED; +import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_HALF_OPENED; +import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_OPENED; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.Surface; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.sysui.ShellInit; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link TabletopModeController}. + */ +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +@SmallTest +public class TabletopModeControllerTest extends ShellTestCase { + // It's considered tabletop mode if the display rotation angle matches what's in this array. + // It's defined as com.android.internal.R.array.config_deviceTabletopRotations on real devices. + private static final int[] TABLETOP_MODE_ROTATIONS = new int[] { + 90 /* Surface.ROTATION_90 */, + 270 /* Surface.ROTATION_270 */ + }; + + private TestShellExecutor mMainExecutor; + + private Configuration mConfiguration; + + private TabletopModeController mPipTabletopController; + + @Mock + private Context mContext; + + @Mock + private ShellInit mShellInit; + + @Mock + private Resources mResources; + + @Mock + private DevicePostureController mDevicePostureController; + + @Mock + private DisplayController mDisplayController; + + @Mock + private TabletopModeController.OnTabletopModeChangedListener mOnTabletopModeChangedListener; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mResources.getIntArray(com.android.internal.R.array.config_deviceTabletopRotations)) + .thenReturn(TABLETOP_MODE_ROTATIONS); + when(mContext.getResources()).thenReturn(mResources); + mMainExecutor = new TestShellExecutor(); + mConfiguration = new Configuration(); + mPipTabletopController = new TabletopModeController(mContext, mShellInit, + mDevicePostureController, mDisplayController, mMainExecutor); + mPipTabletopController.onInit(); + } + + @Test + public void instantiateController_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), eq(mPipTabletopController)); + } + + @Test + public void registerOnTabletopModeChangedListener_notInTabletopMode_callbackFalse() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.registerOnTabletopModeChangedListener( + mOnTabletopModeChangedListener); + + verify(mOnTabletopModeChangedListener, times(1)) + .onTabletopModeChanged(false); + } + + @Test + public void registerOnTabletopModeChangedListener_inTabletopMode_callbackTrue() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.registerOnTabletopModeChangedListener( + mOnTabletopModeChangedListener); + + verify(mOnTabletopModeChangedListener, times(1)) + .onTabletopModeChanged(true); + } + + @Test + public void registerOnTabletopModeChangedListener_notInTabletopModeTwice_callbackOnce() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.registerOnTabletopModeChangedListener( + mOnTabletopModeChangedListener); + clearInvocations(mOnTabletopModeChangedListener); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + verifyZeroInteractions(mOnTabletopModeChangedListener); + } + + // Test cases starting from folded state (DEVICE_POSTURE_CLOSED) + @Test + public void foldedRotation90_halfOpen_scheduleTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + + assertTrue(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void foldedRotation0_halfOpen_noScheduleTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void foldedRotation90_halfOpenThenUnfold_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void foldedRotation90_halfOpenThenFold_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void foldedRotation90_halfOpenThenRotate_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + // Test cases starting from unfolded state (DEVICE_POSTURE_OPENED) + @Test + public void unfoldedRotation90_halfOpen_scheduleTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + + assertTrue(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void unfoldedRotation0_halfOpen_noScheduleTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void unfoldedRotation90_halfOpenThenUnfold_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void unfoldedRotation90_halfOpenThenFold_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void unfoldedRotation90_halfOpenThenRotate_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt index 8f66f4e7e47b..94c064bda763 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt @@ -5,13 +5,16 @@ import android.app.WindowConfiguration import android.graphics.Rect import android.os.IBinder import android.testing.AndroidTestingRunner +import android.view.Display import android.window.WindowContainerToken +import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.Change.CHANGE_DRAG_RESIZING import androidx.test.filters.SmallTest import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_BOTTOM import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_RIGHT import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_TOP import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_UNDEFINED @@ -19,10 +22,11 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.Mockito.any import org.mockito.Mockito.argThat import org.mockito.Mockito.never import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations /** @@ -51,6 +55,8 @@ class TaskPositionerTest : ShellTestCase() { private lateinit var mockDisplayController: DisplayController @Mock private lateinit var mockDisplayLayout: DisplayLayout + @Mock + private lateinit var mockDisplay: Display private lateinit var taskPositioner: TaskPositioner @@ -68,6 +74,9 @@ class TaskPositionerTest : ShellTestCase() { `when`(taskToken.asBinder()).thenReturn(taskBinder) `when`(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout) `when`(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI) + `when`(mockDisplayLayout.getStableBounds(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(STABLE_BOUNDS) + } mockWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply { taskId = TASK_ID @@ -78,6 +87,8 @@ class TaskPositionerTest : ShellTestCase() { displayId = DISPLAY_ID configuration.windowConfiguration.bounds = STARTING_BOUNDS } + mockWindowDecoration.mDisplay = mockDisplay + `when`(mockDisplay.displayId).thenAnswer { DISPLAY_ID } } @Test @@ -451,6 +462,72 @@ class TaskPositionerTest : ShellTestCase() { }) } + fun testDragResize_toDisallowedBounds_freezesAtLimit() { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, // Resize right-bottom corner + STARTING_BOUNDS.right.toFloat(), + STARTING_BOUNDS.bottom.toFloat() + ) + + // Resize the task by 10px to the right and bottom, a valid destination + val newBounds = Rect( + STARTING_BOUNDS.left, + STARTING_BOUNDS.top, + STARTING_BOUNDS.right + 10, + STARTING_BOUNDS.bottom + 10) + taskPositioner.onDragPositioningMove( + newBounds.right.toFloat(), + newBounds.bottom.toFloat() + ) + + // Resize the task by another 10px to the right (allowed) and to just in the disallowed + // area of the Y coordinate. + val newBounds2 = Rect( + newBounds.left, + newBounds.top, + newBounds.right + 10, + DISALLOWED_RESIZE_AREA.top + ) + taskPositioner.onDragPositioningMove( + newBounds2.right.toFloat(), + newBounds2.bottom.toFloat() + ) + + taskPositioner.onDragPositioningEnd(newBounds2.right.toFloat(), newBounds2.bottom.toFloat()) + + // The first resize falls in the allowed area, verify there's a change for it. + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && change.ofBounds(newBounds) + } + }) + // The second resize falls in the disallowed area, verify there's no change for it. + verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && change.ofBounds(newBounds2) + } + }) + // Instead, there should be a change for its allowed portion (the X movement) with the Y + // staying frozen in the last valid resize position. + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && change.ofBounds( + Rect( + newBounds2.left, + newBounds2.top, + newBounds2.right, + newBounds.bottom // Stayed at the first resize destination. + ) + ) + } + }) + } + + private fun WindowContainerTransaction.Change.ofBounds(bounds: Rect): Boolean { + return ((windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0) && + bounds == configuration.windowConfiguration.bounds + } + companion object { private const val TASK_ID = 5 private const val MIN_WIDTH = 10 @@ -458,6 +535,19 @@ class TaskPositionerTest : ShellTestCase() { private const val DENSITY_DPI = 20 private const val DEFAULT_MIN = 40 private const val DISPLAY_ID = 1 + private const val NAVBAR_HEIGHT = 50 + private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600) private val STARTING_BOUNDS = Rect(0, 0, 100, 100) + private val DISALLOWED_RESIZE_AREA = Rect( + DISPLAY_BOUNDS.left, + DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT, + DISPLAY_BOUNDS.right, + DISPLAY_BOUNDS.bottom) + private val STABLE_BOUNDS = Rect( + DISPLAY_BOUNDS.left, + DISPLAY_BOUNDS.top, + DISPLAY_BOUNDS.right, + DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT + ) } } diff --git a/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/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateRotationLockSettingsManager.java b/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateRotationLockSettingsManager.java index 4ed7e19f341d..10b004e1b243 100644 --- a/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateRotationLockSettingsManager.java +++ b/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateRotationLockSettingsManager.java @@ -29,6 +29,7 @@ import android.os.Looper; import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; +import android.util.IndentingPrintWriter; import android.util.Log; import android.util.SparseIntArray; @@ -36,6 +37,7 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Objects; @@ -57,6 +59,7 @@ public final class DeviceStateRotationLockSettingsManager { private final SecureSettings mSecureSettings; private String[] mDeviceStateRotationLockDefaults; private SparseIntArray mDeviceStateRotationLockSettings; + private SparseIntArray mDeviceStateDefaultRotationLockSettings; private SparseIntArray mDeviceStateRotationLockFallbackSettings; private String mLastSettingValue; private List<SettableDeviceState> mSettableDeviceStates; @@ -93,9 +96,7 @@ public final class DeviceStateRotationLockSettingsManager { /** Returns true if device-state based rotation lock settings are enabled. */ public static boolean isDeviceStateRotationLockEnabled(Context context) { return context.getResources() - .getStringArray(R.array.config_perDeviceStateRotationLockDefaults) - .length - > 0; + .getStringArray(R.array.config_perDeviceStateRotationLockDefaults).length > 0; } private void listenForSettingsChange() { @@ -228,6 +229,15 @@ public final class DeviceStateRotationLockSettingsManager { try { key = Integer.parseInt(values[i++]); value = Integer.parseInt(values[i++]); + boolean isPersistedValueIgnored = value == DEVICE_STATE_ROTATION_LOCK_IGNORED; + boolean isDefaultValueIgnored = mDeviceStateDefaultRotationLockSettings.get(key) + == DEVICE_STATE_ROTATION_LOCK_IGNORED; + if (isPersistedValueIgnored != isDefaultValueIgnored) { + Log.w(TAG, "Conflict for ignored device state " + key + + ". Falling back on defaults"); + fallbackOnDefaults(); + return; + } mDeviceStateRotationLockSettings.put(key, value); } catch (NumberFormatException e) { Log.wtf(TAG, "Error deserializing one of the saved settings", e); @@ -276,6 +286,9 @@ public final class DeviceStateRotationLockSettingsManager { } private void persistSettingIfChanged(String newSettingValue) { + Log.v(TAG, "persistSettingIfChanged: " + + "last=" + mLastSettingValue + ", " + + "new=" + newSettingValue); if (TextUtils.equals(mLastSettingValue, newSettingValue)) { return; } @@ -288,6 +301,8 @@ public final class DeviceStateRotationLockSettingsManager { private void loadDefaults() { mSettableDeviceStates = new ArrayList<>(mDeviceStateRotationLockDefaults.length); + mDeviceStateDefaultRotationLockSettings = new SparseIntArray( + mDeviceStateRotationLockDefaults.length); mDeviceStateRotationLockSettings = new SparseIntArray( mDeviceStateRotationLockDefaults.length); mDeviceStateRotationLockFallbackSettings = new SparseIntArray(1); @@ -311,6 +326,7 @@ public final class DeviceStateRotationLockSettingsManager { boolean isSettable = rotationLockSetting != DEVICE_STATE_ROTATION_LOCK_IGNORED; mSettableDeviceStates.add(new SettableDeviceState(deviceState, isSettable)); mDeviceStateRotationLockSettings.put(deviceState, rotationLockSetting); + mDeviceStateDefaultRotationLockSettings.put(deviceState, rotationLockSetting); } catch (NumberFormatException e) { Log.wtf(TAG, "Error parsing settings entry. Entry was: " + entry, e); return; @@ -318,6 +334,22 @@ public final class DeviceStateRotationLockSettingsManager { } } + /** Dumps internal state. */ + public void dump(IndentingPrintWriter pw) { + pw.println("DeviceStateRotationLockSettingsManager"); + pw.increaseIndent(); + pw.println("mDeviceStateRotationLockDefaults: " + Arrays.toString( + mDeviceStateRotationLockDefaults)); + pw.println("mDeviceStateDefaultRotationLockSettings: " + + mDeviceStateDefaultRotationLockSettings); + pw.println("mDeviceStateRotationLockSettings: " + mDeviceStateRotationLockSettings); + pw.println("mDeviceStateRotationLockFallbackSettings: " + + mDeviceStateRotationLockFallbackSettings); + pw.println("mSettableDeviceStates: " + mSettableDeviceStates); + pw.println("mLastSettingValue: " + mLastSettingValue); + pw.decreaseIndent(); + } + /** * Called when the persisted settings have changed, requiring a reinitialization of the * in-memory map. @@ -372,5 +404,13 @@ public final class DeviceStateRotationLockSettingsManager { public int hashCode() { return Objects.hash(mDeviceState, mIsSettable); } + + @Override + public String toString() { + return "SettableDeviceState{" + + "mDeviceState=" + mDeviceState + + ", mIsSettable=" + mIsSettable + + '}'; + } } } diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManagerTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManagerTest.java index 81006dd6b011..0fa15eb6bc0c 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManagerTest.java +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManagerTest.java @@ -33,7 +33,10 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.R; import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManager.SettableDeviceState; +import com.google.common.truth.Expect; + import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -45,6 +48,8 @@ import java.util.List; @RunWith(AndroidJUnit4.class) public class DeviceStateRotationLockSettingsManagerTest { + @Rule public Expect mExpect = Expect.create(); + @Mock private Context mMockContext; @Mock private Resources mMockResources; @@ -117,4 +122,40 @@ public class DeviceStateRotationLockSettingsManagerTest { new SettableDeviceState(/* deviceState= */ 0, /* isSettable= */ false) ).inOrder(); } + + @Test + public void persistedInvalidIgnoredState_returnsDefaults() { + when(mMockResources.getStringArray( + R.array.config_perDeviceStateRotationLockDefaults)).thenReturn( + new String[]{"0:1", "1:0:2", "2:2"}); + // Here 2 has IGNORED, and in the defaults 1 has IGNORED. + persistSettings("0:2:2:0:1:2"); + DeviceStateRotationLockSettingsManager manager = + new DeviceStateRotationLockSettingsManager(mMockContext, mFakeSecureSettings); + + mExpect.that(manager.getRotationLockSetting(0)).isEqualTo(1); + mExpect.that(manager.getRotationLockSetting(1)).isEqualTo(2); + mExpect.that(manager.getRotationLockSetting(2)).isEqualTo(2); + } + + @Test + public void persistedValidValues_returnsPersistedValues() { + when(mMockResources.getStringArray( + R.array.config_perDeviceStateRotationLockDefaults)).thenReturn( + new String[]{"0:1", "1:0:2", "2:2"}); + persistSettings("0:2:1:0:2:1"); + DeviceStateRotationLockSettingsManager manager = + new DeviceStateRotationLockSettingsManager(mMockContext, mFakeSecureSettings); + + mExpect.that(manager.getRotationLockSetting(0)).isEqualTo(2); + mExpect.that(manager.getRotationLockSetting(1)).isEqualTo(1); + mExpect.that(manager.getRotationLockSetting(2)).isEqualTo(1); + } + + private void persistSettings(String value) { + mFakeSecureSettings.putStringForUser( + Settings.Secure.DEVICE_STATE_ROTATION_LOCK, + value, + UserHandle.USER_CURRENT); + } } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt index 17a94b8639d0..296c2ae5cf99 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt @@ -419,7 +419,7 @@ class ActivityLaunchAnimator( internal val delegate: AnimationDelegate init { - delegate = AnimationDelegate(controller, callback, launchAnimator, listener) + delegate = AnimationDelegate(controller, callback, listener, launchAnimator) } @BinderThread @@ -446,10 +446,10 @@ class ActivityLaunchAnimator( constructor( private val controller: Controller, private val callback: Callback, - /** The animator to use to animate the window launch. */ - private val launchAnimator: LaunchAnimator = DEFAULT_LAUNCH_ANIMATOR, /** Listener for animation lifecycle events. */ - private val listener: Listener? = null + private val listener: Listener? = null, + /** The animator to use to animate the window launch. */ + private val launchAnimator: LaunchAnimator = DEFAULT_LAUNCH_ANIMATOR ) : RemoteAnimationDelegate<IRemoteAnimationFinishedCallback> { private val launchContainer = controller.launchContainer private val context = launchContainer.context 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/drawable/qs_media_rec_scrim.xml b/packages/SystemUI/res/drawable/qs_media_rec_scrim.xml new file mode 100644 index 000000000000..de0a6201cb09 --- /dev/null +++ b/packages/SystemUI/res/drawable/qs_media_rec_scrim.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <!-- gradient from 25% in the center to 100% at edges --> + <gradient + android:type="radial" + android:gradientRadius="40%p" + android:startColor="#AE000000" + android:endColor="#00000000" /> +</shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/media_recommendation_view.xml b/packages/SystemUI/res/layout/media_recommendation_view.xml index c54c4e48d13d..a4aeba1dbcd6 100644 --- a/packages/SystemUI/res/layout/media_recommendation_view.xml +++ b/packages/SystemUI/res/layout/media_recommendation_view.xml @@ -22,9 +22,10 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:translationZ="0dp" - android:scaleType="centerCrop" + android:scaleType="matrix" android:adjustViewBounds="true" android:clipToOutline="true" + android:layerType="hardware" android:background="@drawable/bg_smartspace_media_item"/> <!-- App icon --> 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 9ce3f385131f..35fc9b69e1f4 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -196,9 +196,6 @@ <!-- Increased height of a small notification in the status bar --> <dimen name="notification_min_height_increased">146dp</dimen> - <!-- Increased height of a collapsed media notification in the status bar --> - <dimen name="notification_min_height_media">160dp</dimen> - <!-- Height of a small notification in the status bar which was used before android N --> <dimen name="notification_min_height_legacy">64dp</dimen> @@ -1656,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> @@ -1704,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/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java index 6bfaf5e49820..b2add4f661cb 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java @@ -18,7 +18,6 @@ package com.android.systemui.shared.system; import static android.app.ActivityManager.LOCK_TASK_MODE_LOCKED; import static android.app.ActivityManager.LOCK_TASK_MODE_NONE; -import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED; import static android.app.ActivityTaskManager.getService; import android.annotation.NonNull; @@ -45,6 +44,7 @@ import android.os.ServiceManager; import android.os.SystemClock; import android.provider.Settings; import android.util.Log; +import android.view.Display; import android.view.IRecentsAnimationController; import android.view.IRecentsAnimationRunner; import android.view.RemoteAnimationTarget; @@ -112,6 +112,13 @@ public class ActivityManagerWrapper { } /** + * @see #getRunningTasks(boolean , int) + */ + public ActivityManager.RunningTaskInfo[] getRunningTasks(boolean filterOnlyVisibleRecents) { + return getRunningTasks(filterOnlyVisibleRecents, Display.INVALID_DISPLAY); + } + + /** * We ask for {@link #NUM_RECENT_ACTIVITIES_REQUEST} activities because when in split screen, * we'll get back 2 activities for each split app and one for launcher. Launcher might be more * "recently" used than one of the split apps so if we only request 2 tasks, then we might miss @@ -120,10 +127,12 @@ public class ActivityManagerWrapper { * @return an array of up to {@link #NUM_RECENT_ACTIVITIES_REQUEST} running tasks * filtering only for tasks that can be visible in the recent tasks list. */ - public ActivityManager.RunningTaskInfo[] getRunningTasks(boolean filterOnlyVisibleRecents) { + public ActivityManager.RunningTaskInfo[] getRunningTasks(boolean filterOnlyVisibleRecents, + int displayId) { // Note: The set of running tasks from the system is ordered by recency List<ActivityManager.RunningTaskInfo> tasks = - mAtm.getTasks(NUM_RECENT_ACTIVITIES_REQUEST, filterOnlyVisibleRecents); + mAtm.getTasks(NUM_RECENT_ACTIVITIES_REQUEST, + filterOnlyVisibleRecents, /* keepInExtras= */ false, displayId); return tasks.toArray(new RunningTaskInfo[tasks.size()]); } 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 be013770519f..d83af60b1373 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; @@ -676,7 +677,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab public void onTrustManagedChanged(boolean managed, int userId) { Assert.isMainThread(); mUserTrustIsManaged.put(userId, managed); - mUserTrustIsUsuallyManaged.put(userId, mTrustManager.isTrustUsuallyManaged(userId)); + boolean trustUsuallyManaged = mTrustManager.isTrustUsuallyManaged(userId); + mLogger.logTrustUsuallyManagedUpdated(userId, mUserTrustIsUsuallyManaged.get(userId), + trustUsuallyManaged, "onTrustManagedChanged"); + mUserTrustIsUsuallyManaged.put(userId, trustUsuallyManaged); for (int i = 0; i < mCallbacks.size(); i++) { KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); if (cb != null) { @@ -704,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); } @@ -864,7 +874,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private void reportSuccessfulBiometricUnlock(boolean isStrongBiometric, int userId) { mBackgroundExecutor.execute( - () -> mLockPatternUtils.reportSuccessfulBiometricUnlock(isStrongBiometric, userId)); + () -> { + mLogger.logReportSuccessfulBiometricUnlock(isStrongBiometric, userId); + mLockPatternUtils.reportSuccessfulBiometricUnlock(isStrongBiometric, userId); + }); } private void handleFingerprintAuthFailed() { @@ -1840,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); + } } }; @@ -2372,8 +2390,12 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab updateSecondaryLockscreenRequirement(user); List<UserInfo> allUsers = mUserManager.getUsers(); for (UserInfo userInfo : allUsers) { + boolean trustUsuallyManaged = mTrustManager.isTrustUsuallyManaged(userInfo.id); + mLogger.logTrustUsuallyManagedUpdated(userInfo.id, + mUserTrustIsUsuallyManaged.get(userInfo.id), + trustUsuallyManaged, "init from constructor"); mUserTrustIsUsuallyManaged.put(userInfo.id, - mTrustManager.isTrustUsuallyManaged(userInfo.id)); + trustUsuallyManaged); } updateAirplaneModeState(); @@ -2413,10 +2435,14 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } private void updateFaceEnrolled(int userId) { - mIsFaceEnrolled = whitelistIpcs( + Boolean isFaceEnrolled = whitelistIpcs( () -> mFaceManager != null && mFaceManager.isHardwareDetected() && mFaceManager.hasEnrolledTemplates(userId) && mBiometricEnabledForUser.get(userId)); + if (mIsFaceEnrolled != isFaceEnrolled) { + mLogger.logFaceEnrolledUpdated(mIsFaceEnrolled, isFaceEnrolled); + } + mIsFaceEnrolled = isFaceEnrolled; } public boolean isFaceSupported() { @@ -2495,11 +2521,13 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab // If this message exists, we should not authenticate again until this message is // consumed by the handler if (mHandler.hasMessages(MSG_BIOMETRIC_AUTHENTICATION_CONTINUE)) { + mLogger.logHandlerHasAuthContinueMsgs(action); return; } // don't start running fingerprint until they're registered if (!mAuthController.areAllFingerprintAuthenticatorsRegistered()) { + mLogger.d("All FP authenticators not registered, skipping FP listening state update"); return; } final boolean shouldListenForFingerprint = shouldListenForFingerprint(isUdfpsSupported()); @@ -2872,9 +2900,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 = @@ -2923,6 +2949,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) { @@ -3071,8 +3102,13 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab @VisibleForTesting boolean isUnlockWithFingerprintPossible(int userId) { // TODO (b/242022358), make this rely on onEnrollmentChanged event and update it only once. - mIsUnlockWithFingerprintPossible.put(userId, mFpm != null && mFpm.isHardwareDetected() - && !isFingerprintDisabled(userId) && mFpm.hasEnrolledTemplates(userId)); + boolean newFpEnrolled = mFpm != null && mFpm.isHardwareDetected() + && !isFingerprintDisabled(userId) && mFpm.hasEnrolledTemplates(userId); + Boolean oldFpEnrolled = mIsUnlockWithFingerprintPossible.getOrDefault(userId, false); + if (oldFpEnrolled != newFpEnrolled) { + mLogger.logFpEnrolledUpdated(userId, oldFpEnrolled, newFpEnrolled); + } + mIsUnlockWithFingerprintPossible.put(userId, newFpEnrolled); return mIsUnlockWithFingerprintPossible.get(userId); } @@ -3188,7 +3224,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab void handleUserSwitching(int userId, CountDownLatch latch) { Assert.isMainThread(); clearBiometricRecognized(); - mUserTrustIsUsuallyManaged.put(userId, mTrustManager.isTrustUsuallyManaged(userId)); + boolean trustUsuallyManaged = mTrustManager.isTrustUsuallyManaged(userId); + mLogger.logTrustUsuallyManagedUpdated(userId, mUserTrustIsUsuallyManaged.get(userId), + trustUsuallyManaged, "userSwitching"); + mUserTrustIsUsuallyManaged.put(userId, trustUsuallyManaged); for (int i = 0; i < mCallbacks.size(); i++) { KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); if (cb != null) { @@ -3614,7 +3653,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab * Register to receive notifications about general keyguard information * (see {@link KeyguardUpdateMonitorCallback}. * - * @param callback The callback to register + * @param callback The callback to register. Stay away from passing anonymous instances + * as they will likely be dereferenced. Ensure that the callback is a class + * field to persist it. */ public void registerCallback(KeyguardUpdateMonitorCallback callback) { Assert.isMainThread(); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java index 0d4889a4c39f..feff216310df 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java @@ -317,4 +317,9 @@ public class KeyguardUpdateMonitorCallback { * Called when the non-strong biometric state changed. */ public void onNonStrongBiometricAllowedChanged(int userId) { } + + /** + * Called when keyguard is going away or not going away. + */ + public void onKeyguardGoingAway() { } } diff --git a/packages/SystemUI/src/com/android/keyguard/logging/BiometricUnlockLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/BiometricUnlockLogger.kt index bc0bd8c53d26..20f90072161b 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/BiometricUnlockLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/BiometricUnlockLogger.kt @@ -16,6 +16,7 @@ package com.android.keyguard.logging +import android.hardware.biometrics.BiometricSourceType import com.android.systemui.dagger.SysUISingleton import com.android.systemui.log.dagger.BiometricLog import com.android.systemui.plugins.log.LogBuffer @@ -157,6 +158,36 @@ class BiometricUnlockLogger @Inject constructor(@BiometricLog private val logBuf } ) } + + fun deferringAuthenticationDueToSleep( + userId: Int, + biometricSourceType: BiometricSourceType, + alreadyPendingAuth: Boolean + ) { + logBuffer.log( + TAG, + DEBUG, + { + int1 = userId + str1 = biometricSourceType.name + bool2 = alreadyPendingAuth + }, + { + "onBiometricAuthenticated, deferring auth: userId: $int1, " + + "biometricSourceType: $str1, " + + "goingToSleep: true, " + + "mPendingAuthentication != null: $bool2" + } + ) + } + + fun finishedGoingToSleepWithPendingAuth() { + logBuffer.log( + TAG, + LogLevel.DEBUG, + "onFinishedGoingToSleep with pendingAuthenticated != null" + ) + } } private fun wakeAndUnlockModeToString(mode: Int): String { diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt index 379c78ad8d0e..51aca070b180 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt @@ -16,6 +16,7 @@ package com.android.keyguard.logging +import com.android.systemui.biometrics.AuthRippleController import com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController import com.android.systemui.log.dagger.KeyguardLog import com.android.systemui.plugins.log.LogBuffer @@ -120,4 +121,29 @@ constructor( "type=${KeyguardIndicationRotateTextViewController.indicationTypeToString(type)}" } } + + fun notShowingUnlockRipple(keyguardNotShowing: Boolean, unlockNotAllowed: Boolean) { + buffer.log( + AuthRippleController.TAG, + LogLevel.DEBUG, + { + bool1 = keyguardNotShowing + bool2 = unlockNotAllowed + }, + { "Not showing unlock ripple: keyguardNotShowing: $bool1, unlockNotAllowed: $bool2" } + ) + } + + fun showingUnlockRippleAt(x: Int, y: Int, context: String) { + buffer.log( + AuthRippleController.TAG, + LogLevel.DEBUG, + { + int1 = x + int2 = y + str1 = context + }, + { "Showing unlock ripple with center (x, y): ($int1, $int2), context: $str1" } + ) + } } diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt index e53f6adb62a4..2403d1116360 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt @@ -26,6 +26,7 @@ import com.android.keyguard.FaceAuthUiEvent import com.android.keyguard.KeyguardListenModel import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.keyguard.TrustGrantFlags +import com.android.systemui.log.dagger.KeyguardUpdateMonitorLog import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.plugins.log.LogLevel import com.android.systemui.plugins.log.LogLevel.DEBUG @@ -33,18 +34,15 @@ import com.android.systemui.plugins.log.LogLevel.ERROR import com.android.systemui.plugins.log.LogLevel.INFO import com.android.systemui.plugins.log.LogLevel.VERBOSE import com.android.systemui.plugins.log.LogLevel.WARNING -import com.android.systemui.log.dagger.KeyguardUpdateMonitorLog import com.google.errorprone.annotations.CompileTimeConstant import javax.inject.Inject private const val TAG = "KeyguardUpdateMonitorLog" -/** - * Helper class for logging for [com.android.keyguard.KeyguardUpdateMonitor] - */ -class KeyguardUpdateMonitorLogger @Inject constructor( - @KeyguardUpdateMonitorLog private val logBuffer: LogBuffer -) { +/** Helper class for logging for [com.android.keyguard.KeyguardUpdateMonitor] */ +class KeyguardUpdateMonitorLogger +@Inject +constructor(@KeyguardUpdateMonitorLog private val logBuffer: LogBuffer) { fun d(@CompileTimeConstant msg: String) = log(msg, DEBUG) fun e(@CompileTimeConstant msg: String) = log(msg, ERROR) @@ -56,15 +54,16 @@ class KeyguardUpdateMonitorLogger @Inject constructor( fun log(@CompileTimeConstant msg: String, level: LogLevel) = logBuffer.log(TAG, level, msg) fun logActiveUnlockTriggered(reason: String?) { - logBuffer.log("ActiveUnlock", DEBUG, - { str1 = reason }, - { "initiate active unlock triggerReason=$str1" }) + logBuffer.log( + "ActiveUnlock", + DEBUG, + { str1 = reason }, + { "initiate active unlock triggerReason=$str1" } + ) } fun logAuthInterruptDetected(active: Boolean) { - logBuffer.log(TAG, DEBUG, - { bool1 = active }, - { "onAuthInterruptDetected($bool1)" }) + logBuffer.log(TAG, DEBUG, { bool1 = active }, { "onAuthInterruptDetected($bool1)" }) } fun logBroadcastReceived(action: String?) { @@ -72,9 +71,12 @@ class KeyguardUpdateMonitorLogger @Inject constructor( } fun logDeviceProvisionedState(deviceProvisioned: Boolean) { - logBuffer.log(TAG, DEBUG, - { bool1 = deviceProvisioned }, - { "DEVICE_PROVISIONED state = $bool1" }) + logBuffer.log( + TAG, + DEBUG, + { bool1 = deviceProvisioned }, + { "DEVICE_PROVISIONED state = $bool1" } + ) } fun logException(ex: Exception, @CompileTimeConstant logMsg: String) { @@ -82,46 +84,56 @@ class KeyguardUpdateMonitorLogger @Inject constructor( } fun logFaceAcquired(acquireInfo: Int) { - logBuffer.log(TAG, DEBUG, - { int1 = acquireInfo }, - { "Face acquired acquireInfo=$int1" }) + logBuffer.log(TAG, DEBUG, { int1 = acquireInfo }, { "Face acquired acquireInfo=$int1" }) } fun logFaceAuthDisabledForUser(userId: Int) { - logBuffer.log(TAG, DEBUG, - { int1 = userId }, - { "Face authentication disabled by DPM for userId: $int1" }) + logBuffer.log( + TAG, + DEBUG, + { int1 = userId }, + { "Face authentication disabled by DPM for userId: $int1" } + ) } fun logFaceAuthError(msgId: Int, originalErrMsg: String) { - logBuffer.log(TAG, DEBUG, { - str1 = originalErrMsg - int1 = msgId - }, { "Face error received: $str1 msgId= $int1" }) + logBuffer.log( + TAG, + DEBUG, + { + str1 = originalErrMsg + int1 = msgId + }, + { "Face error received: $str1 msgId= $int1" } + ) } fun logFaceAuthForWrongUser(authUserId: Int) { - logBuffer.log(TAG, DEBUG, - { int1 = authUserId }, - { "Face authenticated for wrong user: $int1" }) + logBuffer.log( + TAG, + DEBUG, + { int1 = authUserId }, + { "Face authenticated for wrong user: $int1" } + ) } fun logFaceAuthHelpMsg(msgId: Int, helpMsg: String?) { - logBuffer.log(TAG, DEBUG, { - int1 = msgId - str1 = helpMsg - }, { "Face help received, msgId: $int1 msg: $str1" }) + logBuffer.log( + TAG, + DEBUG, + { + int1 = msgId + str1 = helpMsg + }, + { "Face help received, msgId: $int1 msg: $str1" } + ) } fun logFaceAuthRequested(reason: String?) { - logBuffer.log(TAG, DEBUG, { - str1 = reason - }, { "requestFaceAuth() reason=$str1" }) + logBuffer.log(TAG, DEBUG, { str1 = reason }, { "requestFaceAuth() reason=$str1" }) } fun logFaceAuthSuccess(userId: Int) { - logBuffer.log(TAG, DEBUG, - { int1 = userId }, - { "Face auth succeeded for user $int1" }) + logBuffer.log(TAG, DEBUG, { int1 = userId }, { "Face auth succeeded for user $int1" }) } fun logFaceLockoutReset(@LockoutMode mode: Int) { @@ -133,21 +145,30 @@ class KeyguardUpdateMonitorLogger @Inject constructor( } fun logFaceUnlockPossible(isFaceUnlockPossible: Boolean) { - logBuffer.log(TAG, DEBUG, - { bool1 = isFaceUnlockPossible }, - {"isUnlockWithFacePossible: $bool1"}) + logBuffer.log( + TAG, + DEBUG, + { bool1 = isFaceUnlockPossible }, + { "isUnlockWithFacePossible: $bool1" } + ) } fun logFingerprintAuthForWrongUser(authUserId: Int) { - logBuffer.log(TAG, DEBUG, - { int1 = authUserId }, - { "Fingerprint authenticated for wrong user: $int1" }) + logBuffer.log( + TAG, + DEBUG, + { int1 = authUserId }, + { "Fingerprint authenticated for wrong user: $int1" } + ) } fun logFingerprintDisabledForUser(userId: Int) { - logBuffer.log(TAG, DEBUG, - { int1 = userId }, - { "Fingerprint disabled by DPM for userId: $int1" }) + logBuffer.log( + TAG, + DEBUG, + { int1 = userId }, + { "Fingerprint disabled by DPM for userId: $int1" } + ) } fun logFingerprintLockoutReset(@LockoutMode mode: Int) { @@ -155,16 +176,24 @@ class KeyguardUpdateMonitorLogger @Inject constructor( } fun logFingerprintRunningState(fingerprintRunningState: Int) { - logBuffer.log(TAG, DEBUG, - { int1 = fingerprintRunningState }, - { "fingerprintRunningState: $int1" }) + logBuffer.log( + TAG, + DEBUG, + { int1 = fingerprintRunningState }, + { "fingerprintRunningState: $int1" } + ) } fun logFingerprintSuccess(userId: Int, isStrongBiometric: Boolean) { - logBuffer.log(TAG, DEBUG, { - int1 = userId - bool1 = isStrongBiometric - }, {"Fingerprint auth successful: userId: $int1, isStrongBiometric: $bool1"}) + logBuffer.log( + TAG, + DEBUG, + { + int1 = userId + bool1 = isStrongBiometric + }, + { "Fingerprint auth successful: userId: $int1, isStrongBiometric: $bool1" } + ) } fun logFaceDetected(userId: Int, isStrongBiometric: Boolean) { @@ -182,29 +211,42 @@ class KeyguardUpdateMonitorLogger @Inject constructor( } fun logFingerprintError(msgId: Int, originalErrMsg: String) { - logBuffer.log(TAG, DEBUG, { - str1 = originalErrMsg - int1 = msgId - }, { "Fingerprint error received: $str1 msgId= $int1" }) + logBuffer.log( + TAG, + DEBUG, + { + str1 = originalErrMsg + int1 = msgId + }, + { "Fingerprint error received: $str1 msgId= $int1" } + ) } fun logInvalidSubId(subId: Int) { - logBuffer.log(TAG, INFO, - { int1 = subId }, - { "Previously active sub id $int1 is now invalid, will remove" }) + logBuffer.log( + TAG, + INFO, + { int1 = subId }, + { "Previously active sub id $int1 is now invalid, will remove" } + ) } fun logPrimaryKeyguardBouncerChanged( - primaryBouncerIsOrWillBeShowing: Boolean, - primaryBouncerFullyShown: Boolean + primaryBouncerIsOrWillBeShowing: Boolean, + primaryBouncerFullyShown: Boolean ) { - logBuffer.log(TAG, DEBUG, { - bool1 = primaryBouncerIsOrWillBeShowing - bool2 = primaryBouncerFullyShown - }, { - "handlePrimaryBouncerChanged " + + logBuffer.log( + TAG, + DEBUG, + { + bool1 = primaryBouncerIsOrWillBeShowing + bool2 = primaryBouncerFullyShown + }, + { + "handlePrimaryBouncerChanged " + "primaryBouncerIsOrWillBeShowing=$bool1 primaryBouncerFullyShown=$bool2" - }) + } + ) } fun logKeyguardListenerModel(model: KeyguardListenModel) { @@ -212,98 +254,134 @@ class KeyguardUpdateMonitorLogger @Inject constructor( } fun logKeyguardShowingChanged(showing: Boolean, occluded: Boolean, visible: Boolean) { - logBuffer.log(TAG, DEBUG, { - bool1 = showing - bool2 = occluded - bool3 = visible - }, { - "keyguardShowingChanged(showing=$bool1 occluded=$bool2 visible=$bool3)" - }) + logBuffer.log( + TAG, + DEBUG, + { + bool1 = showing + bool2 = occluded + bool3 = visible + }, + { "keyguardShowingChanged(showing=$bool1 occluded=$bool2 visible=$bool3)" } + ) } fun logMissingSupervisorAppError(userId: Int) { - logBuffer.log(TAG, ERROR, - { int1 = userId }, - { "No Profile Owner or Device Owner supervision app found for User $int1" }) + logBuffer.log( + TAG, + ERROR, + { int1 = userId }, + { "No Profile Owner or Device Owner supervision app found for User $int1" } + ) } fun logPhoneStateChanged(newState: String?) { - logBuffer.log(TAG, DEBUG, - { str1 = newState }, - { "handlePhoneStateChanged($str1)" }) + logBuffer.log(TAG, DEBUG, { str1 = newState }, { "handlePhoneStateChanged($str1)" }) } fun logRegisterCallback(callback: KeyguardUpdateMonitorCallback?) { - logBuffer.log(TAG, VERBOSE, - { str1 = "$callback" }, - { "*** register callback for $str1" }) + logBuffer.log(TAG, VERBOSE, { str1 = "$callback" }, { "*** register callback for $str1" }) } fun logRetryingAfterFaceHwUnavailable(retryCount: Int) { - logBuffer.log(TAG, WARNING, - { int1 = retryCount }, - { "Retrying face after HW unavailable, attempt $int1" }) + logBuffer.log( + TAG, + WARNING, + { int1 = retryCount }, + { "Retrying face after HW unavailable, attempt $int1" } + ) } fun logRetryAfterFpErrorWithDelay(msgId: Int, errString: String?, delay: Int) { - logBuffer.log(TAG, DEBUG, { - int1 = msgId - int2 = delay - str1 = "$errString" - }, { - "Fingerprint scheduling retry auth after $int2 ms due to($int1) -> $str1" - }) + logBuffer.log( + TAG, + DEBUG, + { + int1 = msgId + int2 = delay + str1 = "$errString" + }, + { "Fingerprint scheduling retry auth after $int2 ms due to($int1) -> $str1" } + ) } fun logRetryAfterFpHwUnavailable(retryCount: Int) { - logBuffer.log(TAG, WARNING, - { int1 = retryCount }, - { "Retrying fingerprint attempt: $int1" }) + logBuffer.log( + TAG, + WARNING, + { int1 = retryCount }, + { "Retrying fingerprint attempt: $int1" } + ) } fun logSendPrimaryBouncerChanged( primaryBouncerIsOrWillBeShowing: Boolean, primaryBouncerFullyShown: Boolean, ) { - logBuffer.log(TAG, DEBUG, { - bool1 = primaryBouncerIsOrWillBeShowing - bool2 = primaryBouncerFullyShown - }, { - "sendPrimaryBouncerChanged primaryBouncerIsOrWillBeShowing=$bool1 " + + logBuffer.log( + TAG, + DEBUG, + { + bool1 = primaryBouncerIsOrWillBeShowing + bool2 = primaryBouncerFullyShown + }, + { + "sendPrimaryBouncerChanged primaryBouncerIsOrWillBeShowing=$bool1 " + "primaryBouncerFullyShown=$bool2" - }) + } + ) } fun logServiceStateChange(subId: Int, serviceState: ServiceState?) { - logBuffer.log(TAG, DEBUG, { - int1 = subId - str1 = "$serviceState" - }, { "handleServiceStateChange(subId=$int1, serviceState=$str1)" }) + logBuffer.log( + TAG, + DEBUG, + { + int1 = subId + str1 = "$serviceState" + }, + { "handleServiceStateChange(subId=$int1, serviceState=$str1)" } + ) } fun logServiceStateIntent(action: String?, serviceState: ServiceState?, subId: Int) { - logBuffer.log(TAG, VERBOSE, { - str1 = action - str2 = "$serviceState" - int1 = subId - }, { "action $str1 serviceState=$str2 subId=$int1" }) + logBuffer.log( + TAG, + VERBOSE, + { + str1 = action + str2 = "$serviceState" + int1 = subId + }, + { "action $str1 serviceState=$str2 subId=$int1" } + ) } fun logSimState(subId: Int, slotId: Int, state: Int) { - logBuffer.log(TAG, DEBUG, { - int1 = subId - int2 = slotId - long1 = state.toLong() - }, { "handleSimStateChange(subId=$int1, slotId=$int2, state=$long1)" }) + logBuffer.log( + TAG, + DEBUG, + { + int1 = subId + int2 = slotId + long1 = state.toLong() + }, + { "handleSimStateChange(subId=$int1, slotId=$int2, state=$long1)" } + ) } fun logSimStateFromIntent(action: String?, extraSimState: String?, slotId: Int, subId: Int) { - logBuffer.log(TAG, VERBOSE, { - str1 = action - str2 = extraSimState - int1 = slotId - int2 = subId - }, { "action $str1 state: $str2 slotId: $int1 subid: $int2" }) + logBuffer.log( + TAG, + VERBOSE, + { + str1 = action + str2 = extraSimState + int1 = slotId + int2 = subId + }, + { "action $str1 state: $str2 slotId: $int1 subid: $int2" } + ) } fun logSimUnlocked(subId: Int) { @@ -311,78 +389,98 @@ class KeyguardUpdateMonitorLogger @Inject constructor( } fun logStartedListeningForFace(faceRunningState: Int, faceAuthUiEvent: FaceAuthUiEvent) { - logBuffer.log(TAG, VERBOSE, { - int1 = faceRunningState - str1 = faceAuthUiEvent.reason - str2 = faceAuthUiEvent.extraInfoToString() - }, { "startListeningForFace(): $int1, reason: $str1 $str2" }) + logBuffer.log( + TAG, + VERBOSE, + { + int1 = faceRunningState + str1 = faceAuthUiEvent.reason + str2 = faceAuthUiEvent.extraInfoToString() + }, + { "startListeningForFace(): $int1, reason: $str1 $str2" } + ) } fun logStartedListeningForFaceFromWakeUp(faceRunningState: Int, @WakeReason pmWakeReason: Int) { - logBuffer.log(TAG, VERBOSE, { - int1 = faceRunningState - str1 = PowerManager.wakeReasonToString(pmWakeReason) - }, { "startListeningForFace(): $int1, reason: wakeUp-$str1" }) + logBuffer.log( + TAG, + VERBOSE, + { + int1 = faceRunningState + str1 = PowerManager.wakeReasonToString(pmWakeReason) + }, + { "startListeningForFace(): $int1, reason: wakeUp-$str1" } + ) } fun logStoppedListeningForFace(faceRunningState: Int, faceAuthReason: String) { - logBuffer.log(TAG, VERBOSE, { - int1 = faceRunningState - str1 = faceAuthReason - }, { "stopListeningForFace(): currentFaceRunningState: $int1, reason: $str1" }) + logBuffer.log( + TAG, + VERBOSE, + { + int1 = faceRunningState + str1 = faceAuthReason + }, + { "stopListeningForFace(): currentFaceRunningState: $int1, reason: $str1" } + ) } fun logSubInfo(subInfo: SubscriptionInfo?) { - logBuffer.log(TAG, VERBOSE, - { str1 = "$subInfo" }, - { "SubInfo:$str1" }) + logBuffer.log(TAG, VERBOSE, { str1 = "$subInfo" }, { "SubInfo:$str1" }) } fun logTimeFormatChanged(newTimeFormat: String?) { - logBuffer.log(TAG, DEBUG, - { str1 = newTimeFormat }, - { "handleTimeFormatUpdate timeFormat=$str1" }) + logBuffer.log( + TAG, + DEBUG, + { str1 = newTimeFormat }, + { "handleTimeFormatUpdate timeFormat=$str1" } + ) } fun logUdfpsPointerDown(sensorId: Int) { - logBuffer.log(TAG, DEBUG, - { int1 = sensorId }, - { "onUdfpsPointerDown, sensorId: $int1" }) + logBuffer.log(TAG, DEBUG, { int1 = sensorId }, { "onUdfpsPointerDown, sensorId: $int1" }) } fun logUdfpsPointerUp(sensorId: Int) { - logBuffer.log(TAG, DEBUG, - { int1 = sensorId }, - { "onUdfpsPointerUp, sensorId: $int1" }) + logBuffer.log(TAG, DEBUG, { int1 = sensorId }, { "onUdfpsPointerUp, sensorId: $int1" }) } fun logUnexpectedFaceCancellationSignalState(faceRunningState: Int, unlockPossible: Boolean) { - logBuffer.log(TAG, ERROR, { - int1 = faceRunningState - bool1 = unlockPossible - }, { - "Cancellation signal is not null, high chance of bug in " + - "face auth lifecycle management. " + - "Face state: $int1, unlockPossible: $bool1" - }) + logBuffer.log( + TAG, + ERROR, + { + int1 = faceRunningState + bool1 = unlockPossible + }, + { + "Cancellation signal is not null, high chance of bug in " + + "face auth lifecycle management. " + + "Face state: $int1, unlockPossible: $bool1" + } + ) } fun logUnexpectedFpCancellationSignalState( fingerprintRunningState: Int, unlockPossible: Boolean ) { - logBuffer.log(TAG, ERROR, { - int1 = fingerprintRunningState - bool1 = unlockPossible - }, { - "Cancellation signal is not null, high chance of bug in " + - "fp auth lifecycle management. FP state: $int1, unlockPossible: $bool1" - }) + logBuffer.log( + TAG, + ERROR, + { + int1 = fingerprintRunningState + bool1 = unlockPossible + }, + { + "Cancellation signal is not null, high chance of bug in " + + "fp auth lifecycle management. FP state: $int1, unlockPossible: $bool1" + } + ) } fun logUnregisterCallback(callback: KeyguardUpdateMonitorCallback?) { - logBuffer.log(TAG, VERBOSE, - { str1 = "$callback" }, - { "*** unregister callback for $str1" }) + logBuffer.log(TAG, VERBOSE, { str1 = "$callback" }, { "*** unregister callback for $str1" }) } fun logUserRequestedUnlock( @@ -390,75 +488,173 @@ class KeyguardUpdateMonitorLogger @Inject constructor( reason: String?, dismissKeyguard: Boolean ) { - logBuffer.log("ActiveUnlock", DEBUG, { - str1 = requestOrigin?.name - str2 = reason - bool1 = dismissKeyguard - }, { "reportUserRequestedUnlock origin=$str1 reason=$str2 dismissKeyguard=$bool1" }) + logBuffer.log( + "ActiveUnlock", + DEBUG, + { + str1 = requestOrigin?.name + str2 = reason + bool1 = dismissKeyguard + }, + { "reportUserRequestedUnlock origin=$str1 reason=$str2 dismissKeyguard=$bool1" } + ) } fun logTrustGrantedWithFlags( - flags: Int, - newlyUnlocked: Boolean, - userId: Int, - message: String? + flags: Int, + newlyUnlocked: Boolean, + userId: Int, + message: String? ) { - logBuffer.log(TAG, DEBUG, { - int1 = flags - bool1 = newlyUnlocked - int2 = userId - str1 = message - }, { "trustGrantedWithFlags[user=$int2] newlyUnlocked=$bool1 " + - "flags=${TrustGrantFlags(int1)} message=$str1" }) - } - - fun logTrustChanged( - wasTrusted: Boolean, - isNowTrusted: Boolean, - userId: Int - ) { - logBuffer.log(TAG, DEBUG, { - bool1 = wasTrusted - bool2 = isNowTrusted - int1 = userId - }, { "onTrustChanged[user=$int1] wasTrusted=$bool1 isNowTrusted=$bool2" }) + logBuffer.log( + TAG, + DEBUG, + { + int1 = flags + bool1 = newlyUnlocked + int2 = userId + str1 = message + }, + { + "trustGrantedWithFlags[user=$int2] newlyUnlocked=$bool1 " + + "flags=${TrustGrantFlags(int1)} message=$str1" + } + ) + } + + fun logTrustChanged(wasTrusted: Boolean, isNowTrusted: Boolean, userId: Int) { + logBuffer.log( + TAG, + DEBUG, + { + bool1 = wasTrusted + bool2 = isNowTrusted + int1 = userId + }, + { "onTrustChanged[user=$int1] wasTrusted=$bool1 isNowTrusted=$bool2" } + ) } fun logKeyguardStateUpdate( - secure: Boolean, - canDismissLockScreen: Boolean, - trusted: Boolean, - trustManaged: Boolean - + secure: Boolean, + canDismissLockScreen: Boolean, + trusted: Boolean, + trustManaged: Boolean ) { - logBuffer.log("KeyguardState", DEBUG, { - bool1 = secure - bool2 = canDismissLockScreen - bool3 = trusted - bool4 = trustManaged - }, { "#update secure=$bool1 canDismissKeyguard=$bool2" + - " trusted=$bool3 trustManaged=$bool4" }) + logBuffer.log( + "KeyguardState", + DEBUG, + { + bool1 = secure + bool2 = canDismissLockScreen + bool3 = trusted + bool4 = trustManaged + }, + { + "#update secure=$bool1 canDismissKeyguard=$bool2" + + " trusted=$bool3 trustManaged=$bool4" + } + ) } fun logSkipUpdateFaceListeningOnWakeup(@WakeReason pmWakeReason: Int) { - logBuffer.log(TAG, VERBOSE, { - str1 = PowerManager.wakeReasonToString(pmWakeReason) - }, { "Skip updating face listening state on wakeup from $str1"}) + logBuffer.log( + TAG, + VERBOSE, + { str1 = PowerManager.wakeReasonToString(pmWakeReason) }, + { "Skip updating face listening state on wakeup from $str1" } + ) } fun logTaskStackChangedForAssistant(assistantVisible: Boolean) { - logBuffer.log(TAG, VERBOSE, { - bool1 = assistantVisible - }, { - "TaskStackChanged for ACTIVITY_TYPE_ASSISTANT, assistant visible: $bool1" - }) + logBuffer.log( + TAG, + VERBOSE, + { bool1 = assistantVisible }, + { "TaskStackChanged for ACTIVITY_TYPE_ASSISTANT, assistant visible: $bool1" } + ) } fun logAssistantVisible(assistantVisible: Boolean) { - logBuffer.log(TAG, VERBOSE, { - bool1 = assistantVisible - }, { - "Updating mAssistantVisible to new value: $bool1" - }) + logBuffer.log( + TAG, + VERBOSE, + { bool1 = assistantVisible }, + { "Updating mAssistantVisible to new value: $bool1" } + ) + } + + fun logReportSuccessfulBiometricUnlock(isStrongBiometric: Boolean, userId: Int) { + logBuffer.log( + TAG, + DEBUG, + { + bool1 = isStrongBiometric + int1 = userId + }, + { "reporting successful biometric unlock: isStrongBiometric: $bool1, userId: $int1" } + ) + } + + fun logHandlerHasAuthContinueMsgs(action: Int) { + logBuffer.log( + TAG, + DEBUG, + { int1 = action }, + { + "MSG_BIOMETRIC_AUTHENTICATION_CONTINUE already queued up, " + + "ignoring updating FP listening state to $int1" + } + ) + } + + fun logFaceEnrolledUpdated(oldValue: Boolean, newValue: Boolean) { + logBuffer.log( + TAG, + DEBUG, + { + bool1 = oldValue + bool2 = newValue + }, + { "Face enrolled state changed: old: $bool1, new: $bool2" } + ) + } + + fun logFpEnrolledUpdated(userId: Int, oldValue: Boolean, newValue: Boolean) { + logBuffer.log( + TAG, + DEBUG, + { + int1 = userId + bool1 = oldValue + bool2 = newValue + }, + { "Fp enrolled state changed for userId: $int1 old: $bool1, new: $bool2" } + ) + } + + fun logTrustUsuallyManagedUpdated( + userId: Int, + oldValue: Boolean, + newValue: Boolean, + context: String + ) { + logBuffer.log( + TAG, + DEBUG, + { + int1 = userId + bool1 = oldValue + bool2 = newValue + str1 = context + }, + { + "trustUsuallyManaged changed for " + + "userId: $int1 " + + "old: $bool1, " + + "new: $bool2 " + + "context: $context" + } + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt index d68fcd0a5922..8f6b5c956853 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt @@ -26,6 +26,7 @@ import android.hardware.biometrics.BiometricSourceType import androidx.annotation.VisibleForTesting import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback +import com.android.keyguard.logging.KeyguardLogger import com.android.settingslib.Utils import com.android.systemui.R import com.android.systemui.animation.Interpolators @@ -74,6 +75,7 @@ class AuthRippleController @Inject constructor( private val udfpsControllerProvider: Provider<UdfpsController>, private val statusBarStateController: StatusBarStateController, private val featureFlags: FeatureFlags, + private val logger: KeyguardLogger, rippleView: AuthRippleView? ) : ViewController<AuthRippleView>(rippleView), KeyguardStateController.Callback, WakefulnessLifecycle.Observer { @@ -120,8 +122,11 @@ class AuthRippleController @Inject constructor( } fun showUnlockRipple(biometricSourceType: BiometricSourceType) { - if (!keyguardStateController.isShowing || - !keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(biometricSourceType)) { + val keyguardNotShowing = !keyguardStateController.isShowing + val unlockNotAllowed = !keyguardUpdateMonitor + .isUnlockingWithBiometricAllowed(biometricSourceType) + if (keyguardNotShowing || unlockNotAllowed) { + logger.notShowingUnlockRipple(keyguardNotShowing, unlockNotAllowed) return } @@ -138,6 +143,7 @@ class AuthRippleController @Inject constructor( Math.max(it.y, centralSurfaces.displayHeight.toInt() - it.y) ) ) + logger.showingUnlockRippleAt(it.x, it.y, "FP sensor radius: $udfpsRadius") showUnlockedRipple() } } else if (biometricSourceType == BiometricSourceType.FACE) { @@ -155,6 +161,7 @@ class AuthRippleController @Inject constructor( Math.max(it.y, centralSurfaces.displayHeight.toInt() - it.y) ) ) + logger.showingUnlockRippleAt(it.x, it.y, "Face unlock ripple") showUnlockedRipple() } } @@ -391,5 +398,6 @@ class AuthRippleController @Inject constructor( companion object { const val RIPPLE_ANIMATION_DURATION: Long = 1533 + const val TAG = "AuthRippleController" } } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java index bc0f9950f865..f83885b7bb32 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java @@ -38,6 +38,7 @@ import javax.inject.Inject; public class FalsingDataProvider { private static final long MOTION_EVENT_AGE_MS = 1000; + private static final long DROP_EVENT_THRESHOLD_MS = 50; private static final float THREE_HUNDRED_SIXTY_DEG = (float) (2 * Math.PI); private final int mWidthPixels; @@ -60,6 +61,7 @@ public class FalsingDataProvider { private float mAngle = 0; private MotionEvent mFirstRecentMotionEvent; private MotionEvent mLastMotionEvent; + private boolean mDropLastEvent; private boolean mJustUnlockedWithFace; private boolean mA11YAction; @@ -95,6 +97,12 @@ public class FalsingDataProvider { // Ensure prior gesture was completed. May be a no-op. completePriorGesture(); } + + // Drop the gesture closing event if it is close in time to a previous ACTION_MOVE event. + // The reason is that the closing ACTION_UP event of a swipe can be a bit offseted from the + // previous ACTION_MOVE event and when it happens, it makes some classifiers fail. + mDropLastEvent = shouldDropEvent(motionEvent); + mRecentMotionEvents.addAll(motionEvents); FalsingClassifier.logVerbose("Size: " + mRecentMotionEvents.size()); @@ -129,6 +137,7 @@ public class FalsingDataProvider { mPriorMotionEvents = mRecentMotionEvents; mRecentMotionEvents = new TimeLimitedMotionEventBuffer(MOTION_EVENT_AGE_MS); } + mDropLastEvent = false; mA11YAction = false; } @@ -150,8 +159,18 @@ public class FalsingDataProvider { return mYdpi; } + /** + * Get the {@link MotionEvent}s of the most recent gesture. + * + * Note that this list may not include the last recorded event. + * @see #mDropLastEvent + */ public List<MotionEvent> getRecentMotionEvents() { - return mRecentMotionEvents; + if (!mDropLastEvent || mRecentMotionEvents.isEmpty()) { + return mRecentMotionEvents; + } else { + return mRecentMotionEvents.subList(0, mRecentMotionEvents.size() - 1); + } } public List<MotionEvent> getPriorMotionEvents() { @@ -169,7 +188,12 @@ public class FalsingDataProvider { return mFirstRecentMotionEvent; } - /** Get the last recorded {@link MotionEvent}. */ + /** + * Get the last {@link MotionEvent} of the most recent gesture. + * + * Note that this may be the event prior to the last recorded event. + * @see #mDropLastEvent + */ public MotionEvent getLastMotionEvent() { recalculateData(); return mLastMotionEvent; @@ -236,12 +260,13 @@ public class FalsingDataProvider { return; } - if (mRecentMotionEvents.isEmpty()) { + List<MotionEvent> recentMotionEvents = getRecentMotionEvents(); + if (recentMotionEvents.isEmpty()) { mFirstRecentMotionEvent = null; mLastMotionEvent = null; } else { - mFirstRecentMotionEvent = mRecentMotionEvents.get(0); - mLastMotionEvent = mRecentMotionEvents.get(mRecentMotionEvents.size() - 1); + mFirstRecentMotionEvent = recentMotionEvents.get(0); + mLastMotionEvent = recentMotionEvents.get(recentMotionEvents.size() - 1); } calculateAngleInternal(); @@ -249,6 +274,17 @@ public class FalsingDataProvider { mDirty = false; } + private boolean shouldDropEvent(MotionEvent event) { + if (mRecentMotionEvents.size() < 3) return false; + + MotionEvent lastEvent = mRecentMotionEvents.get(mRecentMotionEvents.size() - 1); + boolean isCompletingGesture = event.getActionMasked() == MotionEvent.ACTION_UP + && lastEvent.getActionMasked() == MotionEvent.ACTION_MOVE; + boolean isRecentEvent = + event.getEventTime() - lastEvent.getEventTime() < DROP_EVENT_THRESHOLD_MS; + return isCompletingGesture && isRecentEvent; + } + private void calculateAngleInternal() { if (mRecentMotionEvents.size() < 2) { mAngle = Float.MAX_VALUE; diff --git a/packages/SystemUI/src/com/android/systemui/classifier/TimeLimitedMotionEventBuffer.java b/packages/SystemUI/src/com/android/systemui/classifier/TimeLimitedMotionEventBuffer.java index e5da38936593..addd8e26490a 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/TimeLimitedMotionEventBuffer.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/TimeLimitedMotionEventBuffer.java @@ -183,7 +183,7 @@ public class TimeLimitedMotionEventBuffer implements List<MotionEvent> { @Override public List<MotionEvent> subList(int fromIndex, int toIndex) { - throw new UnsupportedOperationException(); + return mMotionEvents.subList(fromIndex, toIndex); } class Iter implements ListIterator<MotionEvent> { diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java index c214f5341450..e049ae09b1de 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java @@ -263,10 +263,11 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv @Override // ClipboardListener.ClipboardOverlay public void setClipData(ClipData data, String source) { ClipboardModel model = ClipboardModel.fromClipData(mContext, mClipboardUtils, data, source); - if (mExitAnimator != null && mExitAnimator.isRunning()) { + boolean wasExiting = (mExitAnimator != null && mExitAnimator.isRunning()); + if (wasExiting) { mExitAnimator.cancel(); } - boolean shouldAnimate = !model.dataMatches(mClipboardModel); + boolean shouldAnimate = !model.dataMatches(mClipboardModel) || wasExiting; mClipboardModel = model; mClipboardLogger.setClipSource(mClipboardModel.getSource()); if (shouldAnimate) { @@ -313,15 +314,19 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv mOnPreviewTapped = this::editText; break; case IMAGE: - if (model.isSensitive() || model.loadThumbnail(mContext) != null) { - mView.showImagePreview( - model.isSensitive() ? null : model.loadThumbnail(mContext)); - mView.setEditAccessibilityAction(true); - mOnPreviewTapped = () -> editImage(model.getUri()); - } else { - // image loading failed - mView.showDefaultTextPreview(); - } + mBgExecutor.execute(() -> { + if (model.isSensitive() || model.loadThumbnail(mContext) != null) { + mView.post(() -> { + mView.showImagePreview( + model.isSensitive() ? null : model.loadThumbnail(mContext)); + mView.setEditAccessibilityAction(true); + }); + mOnPreviewTapped = () -> editImage(model.getUri()); + } else { + // image loading failed + mView.post(mView::showDefaultTextPreview); + } + }); break; case URI: case OTHER: @@ -346,9 +351,20 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv } private void animateFromMinimized() { - mIsMinimized = false; - setExpandedView(); - animateIn(); + if (mEnterAnimator != null && mEnterAnimator.isRunning()) { + mEnterAnimator.cancel(); + } + mEnterAnimator = mView.getMinimizedFadeoutAnimation(); + mEnterAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mIsMinimized = false; + setExpandedView(); + animateIn(); + } + }); + mEnterAnimator.start(); } private String getAccessibilityAnnouncement(ClipboardModel.Type type) { @@ -363,15 +379,15 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv private void classifyText(ClipboardModel model) { mBgExecutor.execute(() -> { - Optional<RemoteAction> remoteAction = mClipboardUtils.getAction( - model.getText(), model.getTextLinks(), model.getSource()); + Optional<RemoteAction> remoteAction = + mClipboardUtils.getAction(model.getTextLinks(), model.getSource()); if (model.equals(mClipboardModel)) { remoteAction.ifPresent(action -> { mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_ACTION_SHOWN); - mView.setActionChip(action, () -> { + mView.post(() -> mView.setActionChip(action, () -> { mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED); animateOut(); - }); + })); }); } }); diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtils.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtils.java index a85f8b9357f5..25caaeac2c38 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtils.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtils.java @@ -39,6 +39,9 @@ import javax.inject.Inject; class ClipboardOverlayUtils { + // minimum proportion of entire text an entity must take up, to be considered for smart actions + private static final float MINIMUM_ENTITY_PROPORTION = .8f; + private final TextClassifier mTextClassifier; @Inject @@ -65,19 +68,23 @@ class ClipboardOverlayUtils { return false; } - public Optional<RemoteAction> getAction(CharSequence text, TextLinks textLinks, String source) { - return getActions(text, textLinks).stream().filter(remoteAction -> { + public Optional<RemoteAction> getAction(TextLinks textLinks, String source) { + return getActions(textLinks).stream().filter(remoteAction -> { ComponentName component = remoteAction.getActionIntent().getIntent().getComponent(); return component != null && !TextUtils.equals(source, component.getPackageName()); }).findFirst(); } - private ArrayList<RemoteAction> getActions(CharSequence text, TextLinks textLinks) { + private ArrayList<RemoteAction> getActions(TextLinks textLinks) { ArrayList<RemoteAction> actions = new ArrayList<>(); for (TextLinks.TextLink link : textLinks.getLinks()) { - TextClassification classification = mTextClassifier.classifyText( - text, link.getStart(), link.getEnd(), null); - actions.addAll(classification.getActions()); + // skip classification for incidental entities + if (link.getEnd() - link.getStart() + >= textLinks.getText().length() * MINIMUM_ENTITY_PROPORTION) { + TextClassification classification = mTextClassifier.classifyText( + textLinks.getText(), link.getStart(), link.getEnd(), null); + actions.addAll(classification.getActions()); + } } return actions; } @@ -92,9 +99,13 @@ class ClipboardOverlayUtils { private ArrayList<RemoteAction> getActions(ClipData.Item item) { ArrayList<RemoteAction> actions = new ArrayList<>(); for (TextLinks.TextLink link : item.getTextLinks().getLinks()) { - TextClassification classification = mTextClassifier.classifyText( - item.getText(), link.getStart(), link.getEnd(), null); - actions.addAll(classification.getActions()); + // skip classification for incidental entities + if (link.getEnd() - link.getStart() + >= item.getText().length() * MINIMUM_ENTITY_PROPORTION) { + TextClassification classification = mTextClassifier.classifyText( + item.getText(), link.getStart(), link.getEnd(), null); + actions.addAll(classification.getActions()); + } } return actions; } diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java index f372bb4bc7f2..28c57d31a4f3 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java @@ -21,6 +21,7 @@ import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.annotation.Nullable; @@ -286,6 +287,20 @@ public class ClipboardOverlayView extends DraggableConstraintLayout { mActionChips.clear(); } + Animator getMinimizedFadeoutAnimation() { + ObjectAnimator anim = ObjectAnimator.ofFloat(mMinimizedPreview, "alpha", 1, 0); + anim.setDuration(66); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mMinimizedPreview.setVisibility(View.GONE); + mMinimizedPreview.setAlpha(1); + } + }); + return anim; + } + Animator getEnterAnimation() { if (mAccessibilityManager.isEnabled()) { mDismissButton.setVisibility(View.VISIBLE); 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 f0ee44305b10..05527bd2c843 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -70,11 +70,14 @@ 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; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.connectivity.ConnectivityModule; +import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinder; import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl; @@ -256,9 +259,7 @@ public abstract class SystemUIModule { abstract FingerprintInteractiveToAuthProvider optionalFingerprintInteractiveToAuthProvider(); @BindsOptionalOf - //TODO(b/269430792 remove full qualifier. Full qualifier is used to avoid merge conflict.) - abstract com.android.systemui.statusbar.events.SystemStatusAnimationScheduler - optionalSystemStatusAnimationScheduler(); + abstract SystemStatusAnimationScheduler optionalSystemStatusAnimationScheduler(); @SysUISingleton @Binds @@ -307,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/dreams/complication/ComplicationHostViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java index a2e11b21ea59..24e90f066622 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java @@ -22,14 +22,18 @@ import static com.android.systemui.dreams.complication.dagger.ComplicationModule import android.graphics.Rect; import android.graphics.Region; import android.os.Debug; +import android.os.UserHandle; +import android.provider.Settings; import android.util.Log; import android.view.View; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.lifecycle.LifecycleOwner; +import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.util.ViewController; +import com.android.systemui.util.settings.SecureSettings; import java.util.Collection; import java.util.HashMap; @@ -54,6 +58,8 @@ public class ComplicationHostViewController extends ViewController<ConstraintLay private final LifecycleOwner mLifecycleOwner; private final ComplicationCollectionViewModel mComplicationCollectionViewModel; private final HashMap<ComplicationId, Complication.ViewHolder> mComplications = new HashMap<>(); + @VisibleForTesting + boolean mIsAnimationEnabled; // Whether dream entry animations are finished. private boolean mEntryAnimationsFinished = false; @@ -64,7 +70,8 @@ public class ComplicationHostViewController extends ViewController<ConstraintLay ComplicationLayoutEngine layoutEngine, DreamOverlayStateController dreamOverlayStateController, LifecycleOwner lifecycleOwner, - @Named(SCOPED_COMPLICATIONS_MODEL) ComplicationCollectionViewModel viewModel) { + @Named(SCOPED_COMPLICATIONS_MODEL) ComplicationCollectionViewModel viewModel, + SecureSettings secureSettings) { super(view); mLayoutEngine = layoutEngine; mLifecycleOwner = lifecycleOwner; @@ -78,6 +85,10 @@ public class ComplicationHostViewController extends ViewController<ConstraintLay mDreamOverlayStateController.areEntryAnimationsFinished(); } }); + + // Whether animations are enabled. + mIsAnimationEnabled = secureSettings.getFloatForUser( + Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f, UserHandle.USER_CURRENT) != 0.0f; } @Override @@ -148,7 +159,7 @@ public class ComplicationHostViewController extends ViewController<ConstraintLay // Complications to be added before dream entry animations are finished are set // to invisible and are animated in. - if (!mEntryAnimationsFinished) { + if (!mEntryAnimationsFinished && mIsAnimationEnabled) { view.setVisibility(View.INVISIBLE); } mComplications.put(id, viewHolder); diff --git a/packages/SystemUI/src/com/android/systemui/flags/ConditionalRestarter.kt b/packages/SystemUI/src/com/android/systemui/flags/ConditionalRestarter.kt new file mode 100644 index 000000000000..b20e33a63776 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/flags/ConditionalRestarter.kt @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.flags + +import android.util.Log +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Named +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +/** Restarts the process after all passed in [Condition]s are true. */ +class ConditionalRestarter +@Inject +constructor( + private val systemExitRestarter: SystemExitRestarter, + private val conditions: Set<@JvmSuppressWildcards Condition>, + @Named(RESTART_DELAY) private val restartDelaySec: Long, + @Application private val applicationScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, +) : Restarter { + + private var restartJob: Job? = null + private var pendingReason = "" + private var androidRestartRequested = false + + override fun restartSystemUI(reason: String) { + Log.d(FeatureFlagsDebug.TAG, "SystemUI Restart requested. Restarting when idle.") + scheduleRestart(reason) + } + + override fun restartAndroid(reason: String) { + Log.d(FeatureFlagsDebug.TAG, "Android Restart requested. Restarting when idle.") + androidRestartRequested = true + scheduleRestart(reason) + } + + private fun scheduleRestart(reason: String = "") { + pendingReason = if (reason.isEmpty()) pendingReason else reason + + if (conditions.all { c -> c.canRestartNow(this::scheduleRestart) }) { + if (restartJob == null) { + restartJob = + applicationScope.launch(backgroundDispatcher) { + delay(TimeUnit.SECONDS.toMillis(restartDelaySec)) + restartNow() + } + } + } else { + restartJob?.cancel() + restartJob = null + } + } + + private fun restartNow() { + if (androidRestartRequested) { + systemExitRestarter.restartAndroid(pendingReason) + } else { + systemExitRestarter.restartSystemUI(pendingReason) + } + } + + interface Condition { + /** + * Should return true if the system is ready to restart. + * + * A call to this function means that we want to restart and are waiting for this condition + * to return true. + * + * retryFn should be cached if it is _not_ ready to restart, and later called when it _is_ + * ready to restart. At that point, this method will be called again to verify that the + * system is ready. + * + * Multiple calls to an instance of this method may happen for a single restart attempt if + * multiple [Condition]s are being checked. If any one [Condition] returns false, all the + * [Condition]s will need to be rechecked on the next restart attempt. + */ + fun canRestartNow(retryFn: () -> Unit): Boolean + } + + companion object { + const val RESTART_DELAY = "restarter_restart_delay" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt deleted file mode 100644 index a6956a443e46..000000000000 --- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.flags - -import android.util.Log -import com.android.systemui.keyguard.WakefulnessLifecycle -import javax.inject.Inject - -/** Restarts SystemUI when the screen is locked. */ -class FeatureFlagsDebugRestarter -@Inject -constructor( - private val wakefulnessLifecycle: WakefulnessLifecycle, - private val systemExitRestarter: SystemExitRestarter, -) : Restarter { - - private var androidRestartRequested = false - private var pendingReason = "" - - val observer = - object : WakefulnessLifecycle.Observer { - override fun onFinishedGoingToSleep() { - Log.d(FeatureFlagsDebug.TAG, "Restarting due to systemui flag change") - restartNow() - } - } - - override fun restartSystemUI(reason: String) { - Log.d(FeatureFlagsDebug.TAG, "SystemUI Restart requested. Restarting on next screen off.") - Log.i(FeatureFlagsDebug.TAG, reason) - scheduleRestart(reason) - } - - override fun restartAndroid(reason: String) { - Log.d(FeatureFlagsDebug.TAG, "Android Restart requested. Restarting on next screen off.") - androidRestartRequested = true - scheduleRestart(reason) - } - - fun scheduleRestart(reason: String) { - pendingReason = reason - if (wakefulnessLifecycle.wakefulness == WakefulnessLifecycle.WAKEFULNESS_ASLEEP) { - restartNow() - } else { - wakefulnessLifecycle.addObserver(observer) - } - } - - private fun restartNow() { - if (androidRestartRequested) { - systemExitRestarter.restartAndroid(pendingReason) - } else { - systemExitRestarter.restartSystemUI(pendingReason) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt deleted file mode 100644 index c08266caf147..000000000000 --- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.flags - -import android.util.Log -import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.keyguard.WakefulnessLifecycle -import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP -import com.android.systemui.statusbar.policy.BatteryController -import com.android.systemui.util.concurrency.DelayableExecutor -import java.util.concurrent.TimeUnit -import javax.inject.Inject - -/** Restarts SystemUI when the device appears idle. */ -class FeatureFlagsReleaseRestarter -@Inject -constructor( - private val wakefulnessLifecycle: WakefulnessLifecycle, - private val batteryController: BatteryController, - @Background private val bgExecutor: DelayableExecutor, - private val systemExitRestarter: SystemExitRestarter -) : Restarter { - var listenersAdded = false - var pendingRestart: Runnable? = null - private var pendingReason = "" - var androidRestartRequested = false - - val observer = - object : WakefulnessLifecycle.Observer { - override fun onFinishedGoingToSleep() { - scheduleRestart(pendingReason) - } - } - - val batteryCallback = - object : BatteryController.BatteryStateChangeCallback { - override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) { - scheduleRestart(pendingReason) - } - } - - override fun restartSystemUI(reason: String) { - Log.d( - FeatureFlagsDebug.TAG, - "SystemUI Restart requested. Restarting when plugged in and idle." - ) - scheduleRestart(reason) - } - - override fun restartAndroid(reason: String) { - Log.d( - FeatureFlagsDebug.TAG, - "Android Restart requested. Restarting when plugged in and idle." - ) - androidRestartRequested = true - scheduleRestart(reason) - } - - private fun scheduleRestart(reason: String) { - // Don't bother adding listeners twice. - pendingReason = reason - if (!listenersAdded) { - listenersAdded = true - wakefulnessLifecycle.addObserver(observer) - batteryController.addCallback(batteryCallback) - } - if ( - wakefulnessLifecycle.wakefulness == WAKEFULNESS_ASLEEP && batteryController.isPluggedIn - ) { - if (pendingRestart == null) { - pendingRestart = bgExecutor.executeDelayed(this::restartNow, 30L, TimeUnit.SECONDS) - } - } else if (pendingRestart != null) { - pendingRestart?.run() - pendingRestart = null - } - } - - private fun restartNow() { - Log.d(FeatureFlagsRelease.TAG, "Restarting due to systemui flag change") - if (androidRestartRequested) { - systemExitRestarter.restartAndroid(pendingReason) - } else { - systemExitRestarter.restartSystemUI(pendingReason) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 9b36b923cf88..999dd4804564 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -74,8 +74,12 @@ object Flags { val NOTIFICATION_MEMORY_MONITOR_ENABLED = releasedFlag(112, "notification_memory_monitor_enabled") + /** + * This flag is server-controlled and should stay as [unreleasedFlag] since we never want to + * enable it on release builds. + */ val NOTIFICATION_MEMORY_LOGGING_ENABLED = - unreleasedFlag(119, "notification_memory_logging_enabled", teamfood = true) + unreleasedFlag(119, "notification_memory_logging_enabled") // TODO(b/254512731): Tracking Bug @JvmField val NOTIFICATION_DISMISSAL_FADE = releasedFlag(113, "notification_dismissal_fade") @@ -109,6 +113,13 @@ object Flags { @JvmField val NOTIFICATION_ANIMATE_BIG_PICTURE = unreleasedFlag(120, "notification_animate_big_picture") + @JvmField + 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 = @@ -132,7 +143,7 @@ object Flags { * the digits when the clock moves. */ @JvmField - val STEP_CLOCK_ANIMATION = unreleasedFlag(212, "step_clock_animation", teamfood = true) + val STEP_CLOCK_ANIMATION = releasedFlag(212, "step_clock_animation") /** * Migration from the legacy isDozing/dozeAmount paths to the new KeyguardTransitionRepository @@ -380,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 @@ -441,7 +455,9 @@ object Flags { ) // TODO(b/256873975): Tracking Bug - @JvmField @Keep val WM_BUBBLE_BAR = unreleasedFlag(1111, "wm_bubble_bar") + @JvmField + @Keep + val WM_BUBBLE_BAR = sysPropBooleanFlag(1111, "persist.wm.debug.bubble_bar", default = false) // TODO(b/260271148): Tracking bug @Keep @@ -470,7 +486,14 @@ object Flags { @Keep @JvmField val ENABLE_PIP_APP_ICON_OVERLAY = - sysPropBooleanFlag(1115, "persist.wm.debug.enable_pip_app_icon_overlay", default = false) + 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 @@ -547,7 +570,7 @@ object Flags { // 1500 - chooser aka sharesheet // TODO(b/254512507): Tracking Bug - val CHOOSER_UNBUNDLED = releasedFlag(1500, "chooser_unbundled") + val CHOOSER_UNBUNDLED = unreleasedFlag(1500, "chooser_unbundled") // TODO(b/266983432) Tracking Bug val SHARESHEET_CUSTOM_ACTIONS = @@ -580,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") @@ -652,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", teamfood = true) } diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt index 0054d266c283..3c5012559a89 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt @@ -15,6 +15,7 @@ */ package com.android.systemui.flags +import dagger.Binds import dagger.Module import dagger.Provides import javax.inject.Named @@ -22,6 +23,8 @@ import javax.inject.Named /** Module containing shared code for all FeatureFlag implementations. */ @Module interface FlagsCommonModule { + @Binds fun bindsRestarter(impl: ConditionalRestarter): Restarter + companion object { const val ALL_FLAGS = "all_flags" diff --git a/packages/SystemUI/src/com/android/systemui/flags/PluggedInCondition.kt b/packages/SystemUI/src/com/android/systemui/flags/PluggedInCondition.kt new file mode 100644 index 000000000000..3120638cb17f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/flags/PluggedInCondition.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.flags + +import com.android.systemui.statusbar.policy.BatteryController +import javax.inject.Inject + +/** Returns true when the device is plugged in. */ +class PluggedInCondition +@Inject +constructor( + private val batteryController: BatteryController, +) : ConditionalRestarter.Condition { + + var listenersAdded = false + var retryFn: (() -> Unit)? = null + + val batteryCallback = + object : BatteryController.BatteryStateChangeCallback { + override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) { + retryFn?.invoke() + } + } + + override fun canRestartNow(retryFn: () -> Unit): Boolean { + if (!listenersAdded) { + listenersAdded = true + batteryController.addCallback(batteryCallback) + } + + this.retryFn = retryFn + + return batteryController.isPluggedIn + } +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/ScreenIdleCondition.kt b/packages/SystemUI/src/com/android/systemui/flags/ScreenIdleCondition.kt new file mode 100644 index 000000000000..49e61afbdcd6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/flags/ScreenIdleCondition.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.flags + +import com.android.systemui.keyguard.WakefulnessLifecycle +import javax.inject.Inject + +/** Returns true when the device is "asleep" as defined by the [WakefullnessLifecycle]. */ +class ScreenIdleCondition +@Inject +constructor( + private val wakefulnessLifecycle: WakefulnessLifecycle, +) : ConditionalRestarter.Condition { + + var listenersAdded = false + var retryFn: (() -> Unit)? = null + + val observer = + object : WakefulnessLifecycle.Observer { + override fun onFinishedGoingToSleep() { + retryFn?.invoke() + } + } + + override fun canRestartNow(retryFn: () -> Unit): Boolean { + if (!listenersAdded) { + listenersAdded = true + wakefulnessLifecycle.addObserver(observer) + } + + this.retryFn = retryFn + + return wakefulnessLifecycle.wakefulness == WakefulnessLifecycle.WAKEFULNESS_ASLEEP + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt index b0f9c4edb073..d078688e5944 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt @@ -21,6 +21,7 @@ import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.keyboard.backlight.ui.KeyboardBacklightDialogCoordinator import javax.inject.Inject /** A [CoreStartable] that launches components interested in physical keyboard interaction. */ @@ -28,11 +29,12 @@ import javax.inject.Inject class PhysicalKeyboardCoreStartable @Inject constructor( + private val keyboardBacklightDialogCoordinator: KeyboardBacklightDialogCoordinator, private val featureFlags: FeatureFlags, ) : CoreStartable { override fun start() { if (featureFlags.isEnabled(Flags.KEYBOARD_BACKLIGHT_INDICATOR)) { - // TODO(b/268645743) start listening for keyboard backlight brightness + keyboardBacklightDialogCoordinator.startListening() } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/domain/interactor/KeyboardBacklightInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/domain/interactor/KeyboardBacklightInteractor.kt new file mode 100644 index 000000000000..65e70b319923 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/domain/interactor/KeyboardBacklightInteractor.kt @@ -0,0 +1,41 @@ +/* + * 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.keyboard.backlight.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyboard.data.repository.KeyboardRepository +import com.android.systemui.keyboard.shared.model.BacklightModel +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf + +/** Allows listening to changes to keyboard backlight level */ +@SysUISingleton +class KeyboardBacklightInteractor +@Inject +constructor( + private val keyboardRepository: KeyboardRepository, +) { + + /** Emits current backlight level as [BacklightModel] or null if keyboard is not connected */ + val backlight: Flow<BacklightModel?> = + keyboardRepository.keyboardConnected.flatMapLatest { connected -> + if (connected) keyboardRepository.backlight else flowOf(null) + } +} 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 new file mode 100644 index 000000000000..5e806b612805 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/KeyboardBacklightDialogCoordinator.kt @@ -0,0 +1,66 @@ +/* + * 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.keyboard.backlight.ui + +import android.content.Context +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyboard.backlight.ui.view.KeyboardBacklightDialog +import com.android.systemui.keyboard.backlight.ui.viewmodel.BacklightDialogViewModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * Based on the state produced from [BacklightDialogViewModel] shows or hides keyboard backlight + * indicator + */ +@SysUISingleton +class KeyboardBacklightDialogCoordinator +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + private val context: Context, + private val viewModel: BacklightDialogViewModel, +) { + + var dialog: KeyboardBacklightDialog? = null + + fun startListening() { + applicationScope.launch { + viewModel.dialogContent.collect { dialogViewModel -> + if (dialogViewModel != null) { + if (dialog == null) { + dialog = + KeyboardBacklightDialog( + context, + initialCurrentLevel = dialogViewModel.currentValue, + initialMaxLevel = dialogViewModel.maxValue + ) + dialog?.show() + } else { + dialog?.updateState(dialogViewModel.currentValue, dialogViewModel.maxValue) + } + } else { + dialog?.dismiss() + dialog = null + } + } + } + } +} 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 new file mode 100644 index 000000000000..a173f8b914db --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt @@ -0,0 +1,276 @@ +/* + * 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.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 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, + 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) + 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/keyboard/backlight/ui/viewmodel/BacklightDialogContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogContentViewModel.kt new file mode 100644 index 000000000000..3ef0ca39b8f3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogContentViewModel.kt @@ -0,0 +1,20 @@ +/* + * 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.keyboard.backlight.ui.viewmodel + +data class BacklightDialogContentViewModel(val currentValue: Int, val maxValue: Int) diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogViewModel.kt new file mode 100644 index 000000000000..86abca5faaf3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogViewModel.kt @@ -0,0 +1,72 @@ +/* + * 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.keyboard.backlight.ui.viewmodel + +import android.view.accessibility.AccessibilityManager +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyboard.backlight.domain.interactor.KeyboardBacklightInteractor +import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper +import javax.inject.Inject +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map + +/** + * Responsible for dialog visibility and content - emits [BacklightDialogContentViewModel] if dialog + * should be shown and hidden otherwise + */ +@SysUISingleton +class BacklightDialogViewModel +@Inject +constructor( + interactor: KeyboardBacklightInteractor, + private val accessibilityManagerWrapper: AccessibilityManagerWrapper, +) { + + private val timeoutMillis: Long + get() = + accessibilityManagerWrapper + .getRecommendedTimeoutMillis( + DEFAULT_DIALOG_TIMEOUT_MILLIS, + AccessibilityManager.FLAG_CONTENT_ICONS + ) + .toLong() + + val dialogContent: Flow<BacklightDialogContentViewModel?> = + interactor.backlight + .filterNotNull() + .map { BacklightDialogContentViewModel(it.level, it.maxLevel) } + .timeout(timeoutMillis, emitAfterTimeout = null) + + private fun <T> Flow<T>.timeout(timeoutMillis: Long, emitAfterTimeout: T): Flow<T> { + return flatMapLatest { + flow { + emit(it) + delay(timeoutMillis) + emit(emitAfterTimeout) + } + } + } + + private companion object { + const val DEFAULT_DIALOG_TIMEOUT_MILLIS = 3000 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt index 70faf406d621..9449ece0933b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt @@ -23,7 +23,7 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.keyboard.data.model.BacklightModel +import com.android.systemui.keyboard.shared.model.BacklightModel import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/model/BacklightModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shared/model/BacklightModel.kt index ea15a9f18584..4a32f79285e3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/data/model/BacklightModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shared/model/BacklightModel.kt @@ -15,7 +15,7 @@ * */ -package com.android.systemui.keyboard.data.model +package com.android.systemui.keyboard.shared.model /** * Model for current state of keyboard backlight brightness. [level] indicates current level of 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 d914bf5d8d9a..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; } }; @@ -1920,20 +1932,24 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // If the keyguard is already showing, see if we don't need to bother re-showing it. Check // flags in both files to account for the hiding animation which results in a delay and - // discrepancy between flags. + // discrepancy between flags. If we're in the middle of hiding, do not short circuit so that + // we explicitly re-set state. if (mShowing && mKeyguardStateController.isShowing()) { - if (mPM.isInteractive()) { + if (mPM.isInteractive() && !mHiding) { // It's already showing, and we're not trying to show it while the screen is off. // We can simply reset all of the views. - if (DEBUG) Log.d(TAG, "doKeyguard: not showing because it is already showing"); + if (DEBUG) Log.d(TAG, "doKeyguard: not showing (instead, resetting) because it is " + + "already showing, we're interactive, and we were not previously hiding. " + + "It should be safe to short-circuit here."); resetStateLocked(); return; } else { - // We are trying to show the keyguard while the screen is off - this results from - // race conditions involving locking while unlocking. Don't short-circuit here and - // ensure the keyguard is fully re-shown. + // We are trying to show the keyguard while the screen is off or while we were in + // the middle of hiding - this results from race conditions involving locking while + // unlocking. Don't short-circuit here and ensure the keyguard is fully re-shown. Log.e(TAG, - "doKeyguard: already showing, but re-showing since we're not interactive"); + "doKeyguard: already showing, but re-showing because we're interactive or " + + "were in the middle of hiding."); } } @@ -2427,11 +2443,19 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, if (DEBUG) Log.d(TAG, "handleShow"); } - mHiding = false; mKeyguardExitAnimationRunner = null; mWakeAndUnlocking = false; setPendingLock(false); - setShowingLocked(true); + + // Force if we we're showing in the middle of hiding, to ensure we end up in the correct + // state. + setShowingLocked(true, mHiding /* force */); + if (mHiding) { + Log.d(TAG, "Forcing setShowingLocked because mHiding=true, which means we're " + + "showing in the middle of hiding."); + } + mHiding = false; + mKeyguardViewControllerLazy.get().show(options); resetKeyguardDonePendingLocked(); mHideAnimationRun = false; 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/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt index ca1e27c9d19c..38b9d508f81c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt @@ -47,6 +47,7 @@ class KeyguardTransitionAnimationFlow( duration: Duration, onStep: (Float) -> Float, startTime: Duration = 0.milliseconds, + onStart: (() -> Unit)? = null, onCancel: (() -> Float)? = null, onFinish: (() -> Float)? = null, interpolator: Interpolator = LINEAR, @@ -73,6 +74,7 @@ class KeyguardTransitionAnimationFlow( // the ViewModels of the last update STARTED -> { isComplete = false + onStart?.invoke() max(0f, min(1f, value)) } // Always send a final value of 1. Because of rounding, [value] may never be 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 08907916a8c3..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,10 +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 @@ -34,6 +38,8 @@ class PrimaryBouncerToGoneTransitionViewModel @Inject constructor( private val interactor: KeyguardTransitionInteractor, + private val statusBarStateController: SysuiStatusBarStateController, + private val primaryBouncerInteractor: PrimaryBouncerInteractor, ) { private val transitionAnimation = KeyguardTransitionAnimationFlow( @@ -41,18 +47,50 @@ constructor( transitionFlow = interactor.primaryBouncerToGoneTransition, ) + 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 }, + onStart = { + willRunDismissFromKeyguard = primaryBouncerInteractor.willRunDismissFromKeyguard() + }, + onStep = { + if (willRunDismissFromKeyguard) { + 0f + } else { + 1f - it + } + }, ) - /** Scrim alpha */ - val scrimAlpha: Flow<Float> = - transitionAnimation.createFlow( - duration = TO_GONE_DURATION, - interpolator = EMPHASIZED_ACCELERATE, - onStep = { 1f - it }, - ) + /** Scrim alpha values */ + val scrimAlpha: Flow<ScrimAlpha> = + transitionAnimation + .createFlow( + duration = TO_GONE_DURATION, + interpolator = EMPHASIZED_ACCELERATE, + onStart = { + leaveShadeOpen = statusBarStateController.leaveOpenOnKeyguardHide() + willRunDismissFromKeyguard = + primaryBouncerInteractor.willRunDismissFromKeyguard() + }, + onStep = { 1f - it }, + ) + .map { + if (willRunDismissFromKeyguard) { + ScrimAlpha( + notificationsAlpha = 1f, + ) + } else if (leaveShadeOpen) { + ScrimAlpha( + behindAlpha = 1f, + notificationsAlpha = 1f, + ) + } else { + ScrimAlpha(behindAlpha = it) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/DeviceStateAutoRotationLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/DeviceStateAutoRotationLog.java new file mode 100644 index 000000000000..beb725e61e4a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/DeviceStateAutoRotationLog.java @@ -0,0 +1,30 @@ +/* + * 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.log.dagger; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Qualifier; + +@Qualifier +@Documented +@Retention(RUNTIME) +public @interface DeviceStateAutoRotationLog { +} diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index 642c9f73f625..98b6d70317b8 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -370,6 +370,16 @@ public class LogModule { } /** + * Provides a {@link LogBuffer} for Device State Auto-Rotation logs. + */ + @Provides + @SysUISingleton + @DeviceStateAutoRotationLog + public static LogBuffer provideDeviceStateAutoRotationLogBuffer(LogBufferFactory factory) { + return factory.create("DeviceStateAutoRotationLog", 100); + } + + /** * Provides a {@link LogBuffer} for bluetooth-related logs. */ @Provides 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/media/controls/ui/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt index 680a8b6603d6..67d3be4a3ad2 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt @@ -197,7 +197,6 @@ constructor( private val configListener = object : ConfigurationController.ConfigurationListener { - var lastOrientation = -1 override fun onDensityOrFontScaleChanged() { // System font changes should only happen when UMO is offscreen or a flicker may @@ -214,13 +213,6 @@ constructor( override fun onConfigChanged(newConfig: Configuration?) { if (newConfig == null) return isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL - val newOrientation = newConfig.orientation - if (lastOrientation != newOrientation) { - // The players actually depend on the orientation possibly, so we have to - // recreate them (at least on large screen devices) - lastOrientation = newOrientation - updatePlayers(recreateMedia = true) - } } override fun onUiModeChanged() { diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java index 4ab93daef70c..4ddff530e658 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java @@ -31,12 +31,15 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.ColorStateList; +import android.graphics.Bitmap; import android.graphics.BlendMode; import android.graphics.Color; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; +import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.drawable.Animatable; +import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; @@ -62,7 +65,6 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; -import androidx.appcompat.content.res.AppCompatResources; import androidx.constraintlayout.widget.ConstraintSet; import com.android.internal.annotations.VisibleForTesting; @@ -145,6 +147,12 @@ public class MediaControlPanel { private static final int SMARTSPACE_CARD_CLICK_EVENT = 760; protected static final int SMARTSPACE_CARD_DISMISS_EVENT = 761; + private static final float REC_MEDIA_COVER_SCALE_FACTOR = 1.25f; + private static final float MEDIA_SCRIM_START_ALPHA = 0.25f; + private static final float MEDIA_REC_SCRIM_START_ALPHA = 0.15f; + private static final float MEDIA_PLAYER_SCRIM_END_ALPHA = 0.9f; + private static final float MEDIA_REC_SCRIM_END_ALPHA = 1.0f; + private static final Intent SETTINGS_INTENT = new Intent(ACTION_MEDIA_CONTROLS_SETTINGS); // Buttons to show in small player when using semantic actions @@ -776,7 +784,7 @@ public class MediaControlPanel { WallpaperColors wallpaperColors = getWallpaperColor(artworkIcon); if (wallpaperColors != null) { mutableColorScheme = new ColorScheme(wallpaperColors, true, Style.CONTENT); - artwork = addGradientToIcon(artworkIcon, mutableColorScheme, width, height); + artwork = addGradientToPlayerAlbum(artworkIcon, mutableColorScheme, width, height); isArtworkBound = true; } else { // If there's no artwork, use colors from the app icon @@ -866,8 +874,9 @@ public class MediaControlPanel { Trace.beginAsyncSection(traceName, traceCookie); // Capture width & height from views in foreground for artwork scaling in background - int width = mRecommendationViewHolder.getMediaCoverContainers().get(0).getMeasuredWidth(); - int height = mRecommendationViewHolder.getMediaCoverContainers().get(0).getMeasuredHeight(); + int width = mContext.getResources().getDimensionPixelSize(R.dimen.qs_media_rec_album_width); + int height = mContext.getResources().getDimensionPixelSize( + R.dimen.qs_media_rec_album_height_expanded); mBackgroundExecutor.execute(() -> { // Album art @@ -877,7 +886,8 @@ public class MediaControlPanel { WallpaperColors wallpaperColors = getWallpaperColor(artworkIcon); if (wallpaperColors != null) { mutableColorScheme = new ColorScheme(wallpaperColors, true, Style.CONTENT); - artwork = addGradientToIcon(artworkIcon, mutableColorScheme, width, height); + artwork = addGradientToRecommendationAlbum(artworkIcon, mutableColorScheme, width, + height); } else { artwork = new ColorDrawable(Color.TRANSPARENT); } @@ -886,6 +896,11 @@ public class MediaControlPanel { // Bind the artwork drawable to media cover. ImageView mediaCover = mRecommendationViewHolder.getMediaCoverItems().get(itemIndex); + // Rescale media cover + Matrix coverMatrix = new Matrix(mediaCover.getImageMatrix()); + coverMatrix.postScale(REC_MEDIA_COVER_SCALE_FACTOR, REC_MEDIA_COVER_SCALE_FACTOR, + 0.5f * width, 0.5f * height); + mediaCover.setImageMatrix(coverMatrix); mediaCover.setImageDrawable(artwork); // Set up the app icon. @@ -907,40 +922,62 @@ public class MediaControlPanel { // This method should be called from a background thread. WallpaperColors.fromBitmap takes a // good amount of time. We do that work on the background executor to avoid stalling animations // on the UI Thread. - private WallpaperColors getWallpaperColor(Icon artworkIcon) { + @VisibleForTesting + protected WallpaperColors getWallpaperColor(Icon artworkIcon) { if (artworkIcon != null) { if (artworkIcon.getType() == Icon.TYPE_BITMAP || artworkIcon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) { // Avoids extra processing if this is already a valid bitmap - return WallpaperColors - .fromBitmap(artworkIcon.getBitmap()); + Bitmap artworkBitmap = artworkIcon.getBitmap(); + if (artworkBitmap.isRecycled()) { + Log.d(TAG, "Cannot load wallpaper color from a recycled bitmap"); + return null; + } + return WallpaperColors.fromBitmap(artworkBitmap); } else { Drawable artworkDrawable = artworkIcon.loadDrawable(mContext); if (artworkDrawable != null) { - return WallpaperColors - .fromDrawable(artworkIcon.loadDrawable(mContext)); + return WallpaperColors.fromDrawable(artworkDrawable); } } } return null; } - private LayerDrawable addGradientToIcon( - Icon artworkIcon, - ColorScheme mutableColorScheme, - int width, - int height - ) { + @VisibleForTesting + protected LayerDrawable addGradientToPlayerAlbum(Icon artworkIcon, + ColorScheme mutableColorScheme, int width, int height) { Drawable albumArt = getScaledBackground(artworkIcon, width, height); - GradientDrawable gradient = (GradientDrawable) AppCompatResources - .getDrawable(mContext, R.drawable.qs_media_scrim); + GradientDrawable gradient = (GradientDrawable) mContext.getDrawable( + R.drawable.qs_media_scrim).mutate(); + return setupGradientColorOnDrawable(albumArt, gradient, mutableColorScheme, + MEDIA_SCRIM_START_ALPHA, MEDIA_PLAYER_SCRIM_END_ALPHA); + } + + @VisibleForTesting + protected LayerDrawable addGradientToRecommendationAlbum(Icon artworkIcon, + ColorScheme mutableColorScheme, int width, int height) { + // First try scaling rec card using bitmap drawable. + // If returns null, set drawable bounds. + Drawable albumArt = getScaledRecommendationCover(artworkIcon, width, height); + if (albumArt == null) { + albumArt = getScaledBackground(artworkIcon, width, height); + } + GradientDrawable gradient = (GradientDrawable) mContext.getDrawable( + R.drawable.qs_media_rec_scrim).mutate(); + return setupGradientColorOnDrawable(albumArt, gradient, mutableColorScheme, + MEDIA_REC_SCRIM_START_ALPHA, MEDIA_REC_SCRIM_END_ALPHA); + } + + private LayerDrawable setupGradientColorOnDrawable(Drawable albumArt, GradientDrawable gradient, + ColorScheme mutableColorScheme, float startAlpha, float endAlpha) { gradient.setColors(new int[] { ColorUtilKt.getColorWithAlpha( MediaColorSchemesKt.backgroundStartFromScheme(mutableColorScheme), - 0.25f), + startAlpha), ColorUtilKt.getColorWithAlpha( MediaColorSchemesKt.backgroundEndFromScheme(mutableColorScheme), - 0.9f), + endAlpha), }); return new LayerDrawable(new Drawable[] { albumArt, gradient }); } @@ -1586,6 +1623,29 @@ public class MediaControlPanel { } /** + * Scale artwork to fill the background of media covers in recommendation card. + */ + @UiThread + private Drawable getScaledRecommendationCover(Icon artworkIcon, int width, int height) { + if (width == 0 || height == 0) { + return null; + } + if (artworkIcon != null) { + Bitmap bitmap; + if (artworkIcon.getType() == Icon.TYPE_BITMAP + || artworkIcon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) { + Bitmap artworkBitmap = artworkIcon.getBitmap(); + if (artworkBitmap != null) { + bitmap = Bitmap.createScaledBitmap(artworkIcon.getBitmap(), width, + height, false); + return new BitmapDrawable(mContext.getResources(), bitmap); + } + } + } + return null; + } + + /** * Get the current media controller * * @return the controller diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt index 0788e6172a78..b4724ddebb9a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt @@ -154,9 +154,11 @@ constructor( return transitionLayout?.translationY ?: 0.0f } - /** A callback for RTL config changes */ + /** A callback for config changes */ private val configurationListener = object : ConfigurationController.ConfigurationListener { + var lastOrientation = -1 + override fun onConfigChanged(newConfig: Configuration?) { // Because the TransitionLayout is not always attached (and calculates/caches layout // results regardless of attach state), we have to force the layoutDirection of the @@ -169,6 +171,13 @@ constructor( transitionLayout?.layoutDirection = layoutDirection refreshState() } + val newOrientation = newConfig.orientation + if (lastOrientation != newOrientation) { + // Layout dimensions are possibly changing, so we need to update them. (at + // least on large screen devices) + lastOrientation = newOrientation + loadLayoutForType(type) + } } } } @@ -195,13 +204,14 @@ constructor( * The expanded constraint set used to render a expanded player. If it is modified, make sure to * call [refreshState] */ - val collapsedLayout = ConstraintSet() - + var collapsedLayout = ConstraintSet() + @VisibleForTesting set /** * The expanded constraint set used to render a collapsed player. If it is modified, make sure * to call [refreshState] */ - val expandedLayout = ConstraintSet() + var expandedLayout = ConstraintSet() + @VisibleForTesting set /** Whether the guts are visible for the associated player. */ var isGutsVisible = false @@ -483,7 +493,7 @@ constructor( */ fun attach(transitionLayout: TransitionLayout, type: TYPE) = traceSection("MediaViewController#attach") { - updateMediaViewControllerType(type) + loadLayoutForType(type) logger.logMediaLocation("attach $type", currentStartLocation, currentEndLocation) this.transitionLayout = transitionLayout layoutController.attach(transitionLayout) @@ -641,7 +651,7 @@ constructor( return result } - private fun updateMediaViewControllerType(type: TYPE) { + private fun loadLayoutForType(type: TYPE) { this.type = type // These XML resources contain ConstraintSets that will apply to this player type's layout 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/qs/external/CustomTile.java b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java index 7c2536dac56e..d4854e1a7daf 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java @@ -328,7 +328,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener if (listening) { updateDefaultTileAndIcon(); refreshState(); - if (!mServiceManager.isActiveTile()) { + if (!mServiceManager.isActiveTile() || !isTileReady()) { mServiceManager.setBindRequested(true); mService.onStartListening(); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/DraggableConstraintLayout.java b/packages/SystemUI/src/com/android/systemui/screenshot/DraggableConstraintLayout.java index ead3b7b1de53..0b4b7c691cfd 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/DraggableConstraintLayout.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/DraggableConstraintLayout.java @@ -45,6 +45,7 @@ public class DraggableConstraintLayout extends ConstraintLayout implements ViewTreeObserver.OnComputeInternalInsetsListener { private static final float VELOCITY_DP_PER_MS = 1; + private static final int MAXIMUM_DISMISS_DISTANCE_DP = 400; private final SwipeDismissHandler mSwipeDismissHandler; private final GestureDetector mSwipeDetector; @@ -347,14 +348,18 @@ public class DraggableConstraintLayout extends ConstraintLayout } else { finalX = -1 * getBackgroundRight(); } - float distance = Math.abs(finalX - startX); + float distance = Math.min(Math.abs(finalX - startX), + FloatingWindowUtil.dpToPx(mDisplayMetrics, MAXIMUM_DISMISS_DISTANCE_DP)); + // ensure that view dismisses in the right direction (right in LTR, left in RTL) + float distanceVector = Math.copySign(distance, finalX - startX); anim.addUpdateListener(animation -> { - float translation = MathUtils.lerp(startX, finalX, animation.getAnimatedFraction()); + float translation = MathUtils.lerp( + startX, startX + distanceVector, animation.getAnimatedFraction()); mView.setTranslationX(translation); mView.setAlpha(1 - animation.getAnimatedFraction()); }); - anim.setDuration((long) (distance / Math.abs(velocity))); + anim.setDuration((long) (Math.abs(distance / velocity))); return anim; } 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 e53eea90ad33..c44f4f391831 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -23,6 +23,7 @@ import static android.view.View.VISIBLE; import static androidx.constraintlayout.widget.ConstraintSet.END; import static androidx.constraintlayout.widget.ConstraintSet.PARENT_ID; +import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION; import static com.android.keyguard.KeyguardClockSwitch.LARGE; import static com.android.keyguard.KeyguardClockSwitch.SMALL; import static com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE; @@ -71,6 +72,7 @@ import android.os.VibrationEffect; import android.provider.Settings; import android.transition.ChangeBounds; import android.transition.Transition; +import android.transition.TransitionListenerAdapter; import android.transition.TransitionManager; import android.transition.TransitionSet; import android.transition.TransitionValues; @@ -98,6 +100,7 @@ import android.widget.FrameLayout; import androidx.constraintlayout.widget.ConstraintSet; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.policy.SystemBarUtils; @@ -353,6 +356,7 @@ public final class NotificationPanelViewController implements Dumpable { private final NotificationGutsManager mGutsManager; private final AlternateBouncerInteractor mAlternateBouncerInteractor; private final QuickSettingsController mQsController; + private final InteractionJankMonitor mInteractionJankMonitor; private long mDownTime; private boolean mTouchSlopExceededBeforeDown; @@ -642,6 +646,19 @@ public final class NotificationPanelViewController implements Dumpable { step.getTransitionState() == TransitionState.RUNNING; }; + private final TransitionListenerAdapter mKeyguardStatusAlignmentTransitionListener = + new TransitionListenerAdapter() { + @Override + public void onTransitionCancel(Transition transition) { + mInteractionJankMonitor.cancel(CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION); + } + + @Override + public void onTransitionEnd(Transition transition) { + mInteractionJankMonitor.end(CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION); + } + }; + @Inject public NotificationPanelViewController(NotificationPanelView view, @Main Handler handler, @@ -706,6 +723,7 @@ public final class NotificationPanelViewController implements Dumpable { NotificationStackSizeCalculator notificationStackSizeCalculator, UnlockedScreenOffAnimationController unlockedScreenOffAnimationController, ShadeTransitionController shadeTransitionController, + InteractionJankMonitor interactionJankMonitor, SystemClock systemClock, KeyguardBottomAreaViewModel keyguardBottomAreaViewModel, KeyguardBottomAreaInteractor keyguardBottomAreaInteractor, @@ -720,6 +738,7 @@ public final class NotificationPanelViewController implements Dumpable { DumpManager dumpManager, KeyguardLongPressViewModel keyguardLongPressViewModel, KeyguardInteractor keyguardInteractor) { + mInteractionJankMonitor = interactionJankMonitor; keyguardStateController.addCallback(new KeyguardStateController.Callback() { @Override public void onKeyguardFadingAwayChanged() { @@ -1540,6 +1559,7 @@ public final class NotificationPanelViewController implements Dumpable { int statusConstraint = shouldBeCentered ? PARENT_ID : R.id.qs_edge_guideline; constraintSet.connect(R.id.keyguard_status_view, END, statusConstraint, END); if (animate) { + mInteractionJankMonitor.begin(mView, CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION); ChangeBounds transition = new ChangeBounds(); if (mSplitShadeEnabled) { // Excluding media from the transition on split-shade, as it doesn't transition @@ -1563,6 +1583,7 @@ public final class NotificationPanelViewController implements Dumpable { // The clock container can sometimes be null. If it is, just fall back to the // old animation rather than setting up the custom animations. if (clockContainerView == null || clockContainerView.getChildCount() == 0) { + transition.addListener(mKeyguardStatusAlignmentTransitionListener); TransitionManager.beginDelayedTransition( mNotificationContainerParent, transition); } else { @@ -1581,10 +1602,11 @@ public final class NotificationPanelViewController implements Dumpable { adapter.setDuration(KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION); adapter.addTarget(clockView); set.addTransition(adapter); - + set.addListener(mKeyguardStatusAlignmentTransitionListener); TransitionManager.beginDelayedTransition(mNotificationContainerParent, set); } } else { + transition.addListener(mKeyguardStatusAlignmentTransitionListener); TransitionManager.beginDelayedTransition( mNotificationContainerParent, transition); } @@ -2861,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/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java index c35c5c522798..77550038c94a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java @@ -93,6 +93,16 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi @IntDef({STATE_ICON, STATE_DOT, STATE_HIDDEN}) public @interface VisibleState { } + /** Returns a human-readable string of {@link VisibleState}. */ + public static String getVisibleStateString(@VisibleState int state) { + switch(state) { + case STATE_ICON: return "ICON"; + case STATE_DOT: return "DOT"; + case STATE_HIDDEN: return "HIDDEN"; + default: return "UNKNOWN"; + } + } + private static final String TAG = "StatusBarIconView"; private static final Property<StatusBarIconView, Float> ICON_APPEAR_AMOUNT = new FloatProperty<StatusBarIconView>("iconAppearAmount") { @@ -561,7 +571,8 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi @Override public String toString() { return "StatusBarIconView(" - + "slot='" + mSlot + " alpha=" + getAlpha() + " icon=" + mIcon + + "slot='" + mSlot + "' alpha=" + getAlpha() + " icon=" + mIcon + + " visibleState=" + getVisibleStateString(getVisibleState()) + " iconColor=#" + Integer.toHexString(mIconColor) + " notification=" + mNotification + ')'; } 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 64c1a595483e..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; @@ -191,7 +190,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private int mMaxSmallHeightBeforeS; private int mMaxSmallHeight; private int mMaxSmallHeightLarge; - private int mMaxSmallHeightMedia; private int mMaxExpandedHeight; private int mIncreasedPaddingBetweenElements; private int mNotificationLaunchHeight; @@ -210,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; @@ -1566,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()) { @@ -1771,8 +1752,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView R.dimen.notification_min_height); mMaxSmallHeightLarge = NotificationUtils.getFontScaledHeight(mContext, R.dimen.notification_min_height_increased); - mMaxSmallHeightMedia = NotificationUtils.getFontScaledHeight(mContext, - R.dimen.notification_min_height_media); mMaxExpandedHeight = NotificationUtils.getFontScaledHeight(mContext, R.dimen.notification_max_height); mMaxHeadsUpHeightBeforeN = NotificationUtils.getFontScaledHeight(mContext, @@ -2158,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 @@ -2189,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(); @@ -2201,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) { @@ -2226,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(); @@ -2808,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); } } @@ -2867,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(); @@ -2885,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/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index 39e4000c5d05..4522e41daf91 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -443,6 +443,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder CancellationSignal cancellationSignal = new CancellationSignal(); cancellationSignal.setOnCancelListener( () -> runningInflations.values().forEach(CancellationSignal::cancel)); + return cancellationSignal; } @@ -783,6 +784,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder public static class AsyncInflationTask extends AsyncTask<Void, Void, InflationProgress> implements InflationCallback, InflationTask { + private static final long IMG_PRELOAD_TIMEOUT_MS = 1000L; private final NotificationEntry mEntry; private final Context mContext; private final boolean mInflateSynchronously; @@ -876,7 +878,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder recoveredBuilder, mIsLowPriority, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, packageContext); InflatedSmartReplyState previousSmartReplyState = mRow.getExistingSmartReplyState(); - return inflateSmartReplyViews( + InflationProgress result = inflateSmartReplyViews( inflationProgress, mReInflateFlags, mEntry, @@ -884,6 +886,11 @@ public class NotificationContentInflater implements NotificationRowContentBinder packageContext, previousSmartReplyState, mSmartRepliesInflater); + + // wait for image resolver to finish preloading + mRow.getImageResolver().waitForPreloadedImages(IMG_PRELOAD_TIMEOUT_MS); + + return result; } catch (Exception e) { mError = e; return null; @@ -918,6 +925,9 @@ public class NotificationContentInflater implements NotificationRowContentBinder mCallback.handleInflationException(mRow.getEntry(), new InflationException("Couldn't inflate contentViews" + e)); } + + // Cancel any image loading tasks, not useful any more + mRow.getImageResolver().cancelRunningTasks(); } @Override @@ -944,6 +954,9 @@ public class NotificationContentInflater implements NotificationRowContentBinder // Notify the resolver that the inflation task has finished, // try to purge unnecessary cached entries. mRow.getImageResolver().purgeCache(); + + // Cancel any image loading tasks that have not completed at this point + mRow.getImageResolver().cancelRunningTasks(); } private static class RtlEnabledContext extends ContextWrapper { 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/row/NotificationInlineImageCache.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageCache.java index 41eeada0fcda..fe0b3123eb25 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageCache.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageCache.java @@ -22,8 +22,11 @@ import android.os.AsyncTask; import android.util.Log; import java.util.Set; +import java.util.concurrent.CancellationException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; /** * A cache for inline images of image messages. @@ -56,12 +59,13 @@ public class NotificationInlineImageCache implements NotificationInlineImageReso } @Override - public Drawable get(Uri uri) { + public Drawable get(Uri uri, long timeoutMs) { Drawable result = null; try { - result = mCache.get(uri).get(); - } catch (InterruptedException | ExecutionException ex) { - Log.d(TAG, "get: Failed get image from " + uri); + result = mCache.get(uri).get(timeoutMs, TimeUnit.MILLISECONDS); + } catch (InterruptedException | ExecutionException + | TimeoutException | CancellationException ex) { + Log.d(TAG, "get: Failed get image from " + uri + " " + ex); } return result; } @@ -72,6 +76,15 @@ public class NotificationInlineImageCache implements NotificationInlineImageReso mCache.entrySet().removeIf(entry -> !wantedSet.contains(entry.getKey())); } + @Override + public void cancelRunningTasks() { + mCache.forEach((key, value) -> { + if (value.getStatus() != AsyncTask.Status.FINISHED) { + value.cancel(true); + } + }); + } + private static class PreloadImageTask extends AsyncTask<Uri, Void, Drawable> { private final NotificationInlineImageResolver mResolver; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java index b05e64ab1991..c620f448b3b7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java @@ -23,6 +23,7 @@ import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.Parcelable; +import android.os.SystemClock; import android.util.Log; import com.android.internal.R; @@ -45,6 +46,9 @@ import java.util.Set; public class NotificationInlineImageResolver implements ImageResolver { private static final String TAG = NotificationInlineImageResolver.class.getSimpleName(); + // Timeout for loading images from ImageCache when calling from UI thread + private static final long MAX_UI_THREAD_TIMEOUT_MS = 100L; + private final Context mContext; private final ImageCache mImageCache; private Set<Uri> mWantedUriSet; @@ -123,17 +127,25 @@ public class NotificationInlineImageResolver implements ImageResolver { return null; } + /** + * Loads an image from the Uri. + * This method is synchronous and is usually called from the Main thread. + * It will time-out after MAX_UI_THREAD_TIMEOUT_MS. + * + * @param uri Uri of the target image. + * @return drawable of the image, null if loading failed/timeout + */ @Override public Drawable loadImage(Uri uri) { - return hasCache() ? loadImageFromCache(uri) : resolveImage(uri); + return hasCache() ? loadImageFromCache(uri, MAX_UI_THREAD_TIMEOUT_MS) : resolveImage(uri); } - private Drawable loadImageFromCache(Uri uri) { + private Drawable loadImageFromCache(Uri uri, long timeoutMs) { // if the uri isn't currently cached, try caching it first if (!mImageCache.hasEntry(uri)) { mImageCache.preload((uri)); } - return mImageCache.get(uri); + return mImageCache.get(uri, timeoutMs); } /** @@ -208,6 +220,30 @@ public class NotificationInlineImageResolver implements ImageResolver { } /** + * Wait for a maximum timeout for images to finish preloading + * @param timeoutMs total timeout time + */ + void waitForPreloadedImages(long timeoutMs) { + if (!hasCache()) { + return; + } + Set<Uri> preloadedUris = getWantedUriSet(); + if (preloadedUris != null) { + // Decrement remaining timeout after each image check + long endTimeMs = SystemClock.elapsedRealtime() + timeoutMs; + preloadedUris.forEach( + uri -> loadImageFromCache(uri, endTimeMs - SystemClock.elapsedRealtime())); + } + } + + void cancelRunningTasks() { + if (!hasCache()) { + return; + } + mImageCache.cancelRunningTasks(); + } + + /** * A interface for internal cache implementation of this resolver. */ interface ImageCache { @@ -216,7 +252,7 @@ public class NotificationInlineImageResolver implements ImageResolver { * @param uri The uri of the image. * @return Drawable of the image. */ - Drawable get(Uri uri); + Drawable get(Uri uri, long timeoutMs); /** * Set the image resolver that actually resolves image from specified uri. @@ -241,6 +277,11 @@ public class NotificationInlineImageResolver implements ImageResolver { * Purge unnecessary entries in the cache. */ void purge(); + + /** + * Cancel all unfinished image loading tasks + */ + void cancelRunningTasks(); } } 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 d2087ba6ca1c..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 @@ -67,6 +67,7 @@ import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.view.ViewTreeObserver; import android.view.WindowInsets; +import android.view.WindowInsetsAnimation; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.AnimationUtils; @@ -199,6 +200,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private final boolean mDebugRemoveAnimation; private final boolean mSimplifiedAppearFraction; private final boolean mUseRoundnessSourceTypes; + private boolean mAnimatedInsets; private int mContentHeight; private float mIntrinsicContentHeight; @@ -207,7 +209,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private int mTopPadding; private boolean mAnimateNextTopPaddingChange; private int mBottomPadding; - private int mBottomInset = 0; + @VisibleForTesting + int mBottomInset = 0; private float mQsExpansionFraction; private final int mSplitShadeMinContentHeight; @@ -388,9 +391,33 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } } }; + private boolean mPulsing; private boolean mScrollable; private View mForcedScroll; + private boolean mIsInsetAnimationRunning; + + private final WindowInsetsAnimation.Callback mInsetsCallback = + new WindowInsetsAnimation.Callback( + WindowInsetsAnimation.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE) { + + @Override + public void onPrepare(WindowInsetsAnimation animation) { + mIsInsetAnimationRunning = true; + } + + @Override + public WindowInsets onProgress(WindowInsets windowInsets, + List<WindowInsetsAnimation> list) { + updateBottomInset(windowInsets); + return windowInsets; + } + + @Override + public void onEnd(WindowInsetsAnimation animation) { + mIsInsetAnimationRunning = false; + } + }; /** * @see #setHideAmount(float, float) @@ -584,6 +611,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mDebugRemoveAnimation = featureFlags.isEnabled(Flags.NSSL_DEBUG_REMOVE_ANIMATION); mSimplifiedAppearFraction = featureFlags.isEnabled(Flags.SIMPLIFIED_APPEAR_FRACTION); mUseRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES); + setAnimatedInsetsEnabled(featureFlags.isEnabled(Flags.ANIMATED_NOTIFICATION_SHADE_INSETS)); mSectionsManager = Dependency.get(NotificationSectionsManager.class); mScreenOffAnimationController = Dependency.get(ScreenOffAnimationController.class); @@ -622,6 +650,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mGroupMembershipManager = Dependency.get(GroupMembershipManager.class); mGroupExpansionManager = Dependency.get(GroupExpansionManager.class); setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + if (mAnimatedInsets) { + setWindowInsetsAnimationCallback(mInsetsCallback); + } } /** @@ -690,6 +721,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } @VisibleForTesting + void setAnimatedInsetsEnabled(boolean enabled) { + mAnimatedInsets = enabled; + } + + @VisibleForTesting @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) public void updateFooter() { if (mFooterView == null) { @@ -1781,7 +1817,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable return; } mForcedScroll = v; - scrollTo(v); + if (mAnimatedInsets) { + updateForcedScroll(); + } else { + scrollTo(v); + } } @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) @@ -1813,26 +1853,46 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable + ((!isExpanded() && isPinnedHeadsUp(v)) ? mHeadsUpInset : getTopPadding()); } + private void updateBottomInset(WindowInsets windowInsets) { + mBottomInset = windowInsets.getInsets(WindowInsets.Type.ime()).bottom; + + if (mForcedScroll != null) { + updateForcedScroll(); + } + + int range = getScrollRange(); + if (mOwnScrollY > range) { + setOwnScrollY(range); + } + } + @Override @ShadeViewRefactor(RefactorComponent.COORDINATOR) public WindowInsets onApplyWindowInsets(WindowInsets insets) { - mBottomInset = insets.getInsets(WindowInsets.Type.ime()).bottom; + if (!mAnimatedInsets) { + mBottomInset = insets.getInsets(WindowInsets.Type.ime()).bottom; + } mWaterfallTopInset = 0; final DisplayCutout cutout = insets.getDisplayCutout(); if (cutout != null) { mWaterfallTopInset = cutout.getWaterfallInsets().top; } - - int range = getScrollRange(); - if (mOwnScrollY > range) { - // HACK: We're repeatedly getting staggered insets here while the IME is - // animating away. To work around that we'll wait until things have settled. - removeCallbacks(mReclamp); - postDelayed(mReclamp, 50); - } else if (mForcedScroll != null) { - // The scroll was requested before we got the actual inset - in case we need - // to scroll up some more do so now. - scrollTo(mForcedScroll); + if (mAnimatedInsets && !mIsInsetAnimationRunning) { + // update bottom inset e.g. after rotation + updateBottomInset(insets); + } + if (!mAnimatedInsets) { + int range = getScrollRange(); + if (mOwnScrollY > range) { + // HACK: We're repeatedly getting staggered insets here while the IME is + // animating away. To work around that we'll wait until things have settled. + removeCallbacks(mReclamp); + postDelayed(mReclamp, 50); + } else if (mForcedScroll != null) { + // The scroll was requested before we got the actual inset - in case we need + // to scroll up some more do so now. + scrollTo(mForcedScroll); + } } return insets; } @@ -5163,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/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index 993c4e2a4073..573347cb1aff 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -398,6 +398,9 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp boolean isStrongBiometric) { Trace.beginSection("BiometricUnlockController#onBiometricAuthenticated"); if (mUpdateMonitor.isGoingToSleep()) { + mLogger.deferringAuthenticationDueToSleep(userId, + biometricSourceType, + mPendingAuthenticated != null); mPendingAuthenticated = new PendingAuthenticated(userId, biometricSourceType, isStrongBiometric); Trace.endSection(); @@ -813,6 +816,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp public void onFinishedGoingToSleep() { Trace.beginSection("BiometricUnlockController#onFinishedGoingToSleep"); if (mPendingAuthenticated != null) { + mLogger.finishedGoingToSleepWithPendingAuth(); PendingAuthenticated pendingAuthenticated = mPendingAuthenticated; // Post this to make sure it's executed after the device is fully locked. mHandler.post(() -> onBiometricAuthenticated(pendingAuthenticated.userId, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java index c72eb054c62c..39b5b5a4cef8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java @@ -40,6 +40,7 @@ import com.android.systemui.statusbar.StatusIconDisplayable; import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState; +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger; import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView; import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel; import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView; @@ -288,10 +289,14 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da * @param mobileContext possibly mcc/mnc overridden mobile context * @param subId the subscriptionId for this mobile view */ - public void addModernMobileView(Context mobileContext, int subId) { + public void addModernMobileView( + Context mobileContext, + MobileViewLogger mobileViewLogger, + int subId) { Log.d(TAG, "addModernMobileView (subId=" + subId + ")"); ModernStatusBarMobileView view = ModernStatusBarMobileView.constructAndBind( mobileContext, + mobileViewLogger, "mobile", mMobileIconsViewModel.viewModelForSub(subId, mLocation) ); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java index b88531e59568..ae715b3f20c8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java @@ -429,7 +429,6 @@ public class DozeParameters implements } dispatchAlwaysOnEvent(); - mScreenOffAnimationController.onAlwaysOnChanged(getAlwaysOn()); } @Override @@ -469,6 +468,7 @@ public class DozeParameters implements for (Callback callback : mCallbacks) { callback.onAlwaysOnChange(); } + mScreenOffAnimationController.onAlwaysOnChanged(getAlwaysOn()); } private boolean getPostureSpecificBool( 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 c01137aad408..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,14 +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.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; @@ -243,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; @@ -261,26 +267,22 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump private KeyguardTransitionInteractor mKeyguardTransitionInteractor; private CoroutineDispatcher mMainDispatcher; - private boolean mIsBouncerToGoneTransitionStarted = false; private boolean mIsBouncerToGoneTransitionRunning = false; private PrimaryBouncerToGoneTransitionViewModel mPrimaryBouncerToGoneTransitionViewModel; - private final Consumer<Float> mScrimAlphaConsumer = - (Float alpha) -> { - mScrimInFront.setViewAlpha(0f); - mNotificationsScrim.setViewAlpha(0f); - mScrimBehind.setViewAlpha(alpha); - }; - final Consumer<TransitionStep> mPrimaryBouncerToGoneTransition = - (TransitionStep step) -> { - mIsBouncerToGoneTransitionRunning = - step.getTransitionState() == TransitionState.RUNNING; - mIsBouncerToGoneTransitionStarted = - step.getTransitionState() == TransitionState.STARTED; - if (mIsBouncerToGoneTransitionStarted) { - transitionTo(ScrimState.UNLOCKED); - } + private final Consumer<ScrimAlpha> mScrimAlphaConsumer = + (ScrimAlpha alphas) -> { + mInFrontAlpha = alphas.getFrontAlpha(); + mScrimInFront.setViewAlpha(mInFrontAlpha); + + mNotificationsAlpha = alphas.getNotificationsAlpha(); + mNotificationsScrim.setViewAlpha(mNotificationsAlpha); + + mBehindAlpha = alphas.getBehindAlpha(); + mScrimBehind.setViewAlpha(mBehindAlpha); }; + Consumer<TransitionStep> mPrimaryBouncerToGoneTransition; + @Inject public ScrimController( LightBarController lightBarController, @@ -298,8 +300,12 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump StatusBarKeyguardViewManager statusBarKeyguardViewManager, PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel, KeyguardTransitionInteractor keyguardTransitionInteractor, - @Main CoroutineDispatcher mainDispatcher) { + @Main CoroutineDispatcher mainDispatcher, + LargeScreenShadeInterpolator largeScreenShadeInterpolator, + FeatureFlags featureFlags) { mScrimStateListener = lightBarController::setScrimState; + mLargeScreenShadeInterpolator = largeScreenShadeInterpolator; + mFeatureFlags = featureFlags; mDefaultScrimAlpha = BUSY_SCRIM_ALPHA; mKeyguardStateController = keyguardStateController; @@ -380,6 +386,28 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump state.prepare(state); } + // Directly control transition to UNLOCKED scrim state from PRIMARY_BOUNCER, and make sure + // to report back that keyguard has faded away. This fixes cases where the scrim state was + // rapidly switching on unlock, due to shifts in state in CentralSurfacesImpl + mPrimaryBouncerToGoneTransition = + (TransitionStep step) -> { + TransitionState state = step.getTransitionState(); + + mIsBouncerToGoneTransitionRunning = state == TransitionState.RUNNING; + + if (state == TransitionState.STARTED) { + setExpansionAffectsAlpha(false); + transitionTo(ScrimState.UNLOCKED); + } + + if (state == TransitionState.FINISHED || state == TransitionState.CANCELED) { + setExpansionAffectsAlpha(true); + if (mKeyguardStateController.isKeyguardFadingAway()) { + mStatusBarKeyguardViewManager.onKeyguardFadedAway(); + } + } + }; + collectFlow(behindScrim, mKeyguardTransitionInteractor.getPrimaryBouncerToGoneTransition(), mPrimaryBouncerToGoneTransition, mMainDispatcher); collectFlow(behindScrim, mPrimaryBouncerToGoneTransitionViewModel.getScrimAlpha(), @@ -819,22 +847,26 @@ 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 .constrainedMap(0f, 1f, 0f, 0.3f, mPanelExpansionFraction); - if (!mIsBouncerToGoneTransitionStarted) { - mBehindAlpha = behindFraction * mDefaultScrimAlpha; - } + mBehindAlpha = behindFraction * mDefaultScrimAlpha; // Delay fade-in of notification scrim a bit further, to coincide with the // behind scrim finishing fading in. // Also to coincide with the view starting to fade in, otherwise the empty diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java index 11863627218e..04cc8ce792d1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java @@ -569,7 +569,10 @@ public interface StatusBarIconController { mGroup.addView(view, index, onCreateLayoutParams()); if (mIsInDemoMode) { - mDemoStatusIcons.addModernMobileView(mContext, subId); + mDemoStatusIcons.addModernMobileView( + mContext, + mMobileIconsViewModel.getLogger(), + subId); } return view; @@ -601,6 +604,7 @@ public interface StatusBarIconController { return ModernStatusBarMobileView .constructAndBind( mobileContext, + mMobileIconsViewModel.getLogger(), slot, mMobileIconsViewModel.viewModelForSub(subId, mLocation) ); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java index f6c0da8da8c0..833cb93f62e9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java @@ -79,6 +79,18 @@ public class StatusBarIconHolder { private @IconType int mType = TYPE_ICON; private int mTag = 0; + /** Returns a human-readable string representing the given type. */ + public static String getTypeString(@IconType int type) { + switch(type) { + case TYPE_ICON: return "ICON"; + case TYPE_WIFI: return "WIFI_OLD"; + case TYPE_MOBILE: return "MOBILE_OLD"; + case TYPE_MOBILE_NEW: return "MOBILE_NEW"; + case TYPE_WIFI_NEW: return "WIFI_NEW"; + default: return "UNKNOWN"; + } + } + private StatusBarIconHolder() { } @@ -230,4 +242,11 @@ public class StatusBarIconHolder { public int getTag() { return mTag; } + + @Override + public String toString() { + return "StatusBarIconHolder(type=" + getTypeString(mType) + + " tag=" + getTag() + + " visible=" + isVisible() + ")"; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconList.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconList.java index 8800b05fadb7..565481a20d95 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconList.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconList.java @@ -27,6 +27,7 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; /** A class holding the list of all the system icons that could be shown in the status bar. */ public class StatusBarIconList { @@ -302,7 +303,7 @@ public class StatusBarIconList { @Override public String toString() { - return String.format("(%s) %s", mName, subSlotsString()); + return String.format("(%s) holder=%s %s", mName, mHolder, subSlotsString()); } private String subSlotsString() { @@ -310,7 +311,10 @@ public class StatusBarIconList { return ""; } - return "" + mSubSlots.size() + " subSlots"; + return "| " + mSubSlots.size() + " subSlots: " + + mSubSlots.stream() + .map(StatusBarIconHolder::toString) + .collect(Collectors.joining("|")); } } } 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/StatusBarMoveFromCenterAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationController.kt index 79c0984d9bf7..d30d0e25bd8c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationController.kt @@ -22,6 +22,7 @@ import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator.AlphaP import com.android.systemui.statusbar.phone.PhoneStatusBarViewController.StatusBarViewsCenterProvider import com.android.systemui.unfold.SysUIUnfoldScope import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener +import com.android.systemui.unfold.util.CurrentActivityTypeProvider import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider import javax.inject.Inject import kotlin.math.max @@ -29,9 +30,13 @@ import kotlin.math.max @SysUIUnfoldScope class StatusBarMoveFromCenterAnimationController @Inject constructor( private val progressProvider: ScopedUnfoldTransitionProgressProvider, + private val currentActivityTypeProvider: CurrentActivityTypeProvider, windowManager: WindowManager ) { + // Whether we're on home activity. Updated only when the animation starts. + private var isOnHomeActivity: Boolean? = null + private val transitionListener = TransitionListener() private val moveFromCenterAnimator = UnfoldMoveFromCenterAnimator( windowManager, @@ -60,6 +65,10 @@ class StatusBarMoveFromCenterAnimationController @Inject constructor( } private inner class TransitionListener : TransitionProgressListener { + override fun onTransitionStarted() { + isOnHomeActivity = currentActivityTypeProvider.isHomeActivity + } + override fun onTransitionProgress(progress: Float) { moveFromCenterAnimator.onTransitionProgress(progress) } @@ -68,11 +77,23 @@ class StatusBarMoveFromCenterAnimationController @Inject constructor( // Reset translations when transition is stopped/cancelled // (e.g. the transition could be cancelled mid-way when rotating the screen) moveFromCenterAnimator.onTransitionProgress(1f) + isOnHomeActivity = null } } - private class StatusBarIconsAlphaProvider : AlphaProvider { + + /** + * In certain cases, an alpha is applied based on the progress. + * + * This mainly happens to hide the statusbar during the unfold animation while on apps, as the + * bounds of the app "collapse" to the center, but the statusbar doesn't. + * While on launcher, this alpha is not applied. + */ + private inner class StatusBarIconsAlphaProvider : AlphaProvider { override fun getAlpha(progress: Float): Float { + if (isOnHomeActivity == true) { + return 1.0f + } return max( 0f, (progress - ICONS_START_APPEARING_PROGRESS) / (1 - ICONS_START_APPEARING_PROGRESS) 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/dagger/MobileViewLog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/MobileViewLog.kt new file mode 100644 index 000000000000..e594a8a5efd9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/MobileViewLog.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.statusbar.pipeline.dagger + +import javax.inject.Qualifier + +/** Logs for changes with the new mobile views. */ +@Qualifier +@MustBeDocumented +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +annotation class MobileViewLog diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index 44647515a6e5..adfea80715a2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -148,5 +148,19 @@ abstract class StatusBarPipelineModule { fun provideMobileInputLogBuffer(factory: LogBufferFactory): LogBuffer { return factory.create("MobileInputLog", 100) } + + @Provides + @SysUISingleton + @MobileViewLog + fun provideMobileViewLogBuffer(factory: LogBufferFactory): LogBuffer { + return factory.create("MobileViewLog", 100) + } + + @Provides + @SysUISingleton + @VerboseMobileViewLog + fun provideVerboseMobileViewLogBuffer(factory: LogBufferFactory): LogBuffer { + return factory.create("VerboseMobileViewLog", 100) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/VerboseMobileViewLog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/VerboseMobileViewLog.kt new file mode 100644 index 000000000000..b98789807dd3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/VerboseMobileViewLog.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.statusbar.pipeline.dagger + +import javax.inject.Qualifier + +/** Logs for **verbose** changes with the new mobile views. */ +@Qualifier +@MustBeDocumented +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +annotation class VerboseMobileViewLog diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/shared/MobileInputLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt index 3cbd2b76c248..4156fc152602 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/shared/MobileInputLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.statusbar.pipeline.mobile.shared +package com.android.systemui.statusbar.pipeline.mobile.data import android.net.Network import android.net.NetworkCapabilities @@ -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) { @@ -133,24 +133,6 @@ constructor( ) } - fun logUiAdapterSubIdsUpdated(subs: List<Int>) { - buffer.log( - TAG, - LogLevel.INFO, - { str1 = subs.toString() }, - { "Sub IDs in MobileUiAdapter updated internally: $str1" }, - ) - } - - fun logUiAdapterSubIdsSentToIconController(subs: List<Int>) { - buffer.log( - TAG, - LogLevel.INFO, - { str1 = subs.toString() }, - { "Sub IDs in MobileUiAdapter being sent to icon controller: $str1" }, - ) - } - fun logCarrierConfigChanged(subId: Int) { buffer.log( TAG, 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/CarrierConfigRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepository.kt index bb3b9b2166c3..efdce062bb37 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepository.kt @@ -30,8 +30,8 @@ import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dump.DumpManager +import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfig -import com.android.systemui.statusbar.pipeline.mobile.shared.MobileInputLogger import java.io.PrintWriter import javax.inject.Inject import kotlinx.coroutines.CoroutineScope 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 96b96f14d6aa..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 @@ -36,7 +37,8 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall 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.model.MobileConnectionModel +import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger +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 @@ -47,8 +49,8 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameMo import com.android.systemui.statusbar.pipeline.mobile.data.repository.CarrierConfigRepository 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.shared.MobileInputLogger 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 53a208cd171e..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 @@ -45,11 +45,11 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.statusbar.pipeline.dagger.MobileSummaryLog +import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository -import com.android.systemui.statusbar.pipeline.mobile.shared.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel @@ -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/mobile/ui/MobileUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt index da63ab10f733..075e6ec11ae7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt @@ -22,7 +22,6 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.statusbar.phone.StatusBarIconController import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor -import com.android.systemui.statusbar.pipeline.mobile.shared.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel import java.io.PrintWriter import javax.inject.Inject @@ -55,17 +54,14 @@ constructor( interactor: MobileIconsInteractor, private val iconController: StatusBarIconController, private val iconsViewModelFactory: MobileIconsViewModel.Factory, - private val logger: MobileInputLogger, + private val logger: MobileViewLogger, @Application private val scope: CoroutineScope, private val statusBarPipelineFlags: StatusBarPipelineFlags, ) : CoreStartable { private val mobileSubIds: Flow<List<Int>> = - interactor.filteredSubscriptions - .mapLatest { subscriptions -> - subscriptions.map { subscriptionModel -> subscriptionModel.subscriptionId } - } - .distinctUntilChanged() - .onEach { logger.logUiAdapterSubIdsUpdated(it) } + interactor.filteredSubscriptions.mapLatest { subscriptions -> + subscriptions.map { subscriptionModel -> subscriptionModel.subscriptionId } + } /** * We expose the list of tracked subscriptions as a flow of a list of ints, where each int is @@ -75,7 +71,10 @@ constructor( * NOTE: this should go away as the view presenter learns more about this data pipeline */ private val mobileSubIdsState: StateFlow<List<Int>> = - mobileSubIds.stateIn(scope, SharingStarted.WhileSubscribed(), listOf()) + mobileSubIds + .distinctUntilChanged() + .onEach { logger.logUiAdapterSubIdsUpdated(it) } + .stateIn(scope, SharingStarted.WhileSubscribed(), listOf()) /** In order to keep the logs tame, we will reuse the same top-level mobile icons view model */ val mobileIconsViewModel = iconsViewModelFactory.create(mobileSubIdsState) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt new file mode 100644 index 000000000000..90dff23c637c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt @@ -0,0 +1,118 @@ +/* + * 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.ui + +import android.view.View +import com.android.systemui.Dumpable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dump.DumpManager +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.statusbar.pipeline.dagger.MobileViewLog +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel +import java.io.PrintWriter +import javax.inject.Inject + +/** Logs for changes with the new mobile views. */ +@SysUISingleton +class MobileViewLogger +@Inject +constructor( + @MobileViewLog private val buffer: LogBuffer, + dumpManager: DumpManager, +) : Dumpable { + init { + dumpManager.registerNormalDumpable(this) + } + + private val collectionStatuses = mutableMapOf<String, Boolean>() + + fun logUiAdapterSubIdsUpdated(subs: List<Int>) { + buffer.log( + TAG, + LogLevel.INFO, + { str1 = subs.toString() }, + { "Sub IDs in MobileUiAdapter updated internally: $str1" }, + ) + } + + fun logUiAdapterSubIdsSentToIconController(subs: List<Int>) { + buffer.log( + TAG, + LogLevel.INFO, + { str1 = subs.toString() }, + { "Sub IDs in MobileUiAdapter being sent to icon controller: $str1" }, + ) + } + + fun logNewViewBinding(view: View, viewModel: LocationBasedMobileViewModel) { + buffer.log( + TAG, + LogLevel.INFO, + { + str1 = view.getIdForLogging() + str2 = viewModel.getIdForLogging() + str3 = viewModel.locationName + }, + { "New view binding. viewId=$str1, viewModelId=$str2, viewModelLocation=$str3" }, + ) + } + + fun logCollectionStarted(view: View, viewModel: LocationBasedMobileViewModel) { + collectionStatuses[view.getIdForLogging()] = true + buffer.log( + TAG, + LogLevel.INFO, + { + str1 = view.getIdForLogging() + str2 = viewModel.getIdForLogging() + str3 = viewModel.locationName + }, + { "Collection started. viewId=$str1, viewModelId=$str2, viewModelLocation=$str3" }, + ) + } + + fun logCollectionStopped(view: View, viewModel: LocationBasedMobileViewModel) { + collectionStatuses[view.getIdForLogging()] = false + buffer.log( + TAG, + LogLevel.INFO, + { + str1 = view.getIdForLogging() + str2 = viewModel.getIdForLogging() + str3 = viewModel.locationName + }, + { "Collection stopped. viewId=$str1, viewModelId=$str2, viewModelLocation=$str3" }, + ) + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.println("Collection statuses per view:---") + collectionStatuses.forEach { viewId, isCollecting -> + pw.println("viewId=$viewId, isCollecting=$isCollecting") + } + } + + companion object { + fun Any.getIdForLogging(): String { + // The identityHashCode is guaranteed to be constant for the lifetime of the object. + return Integer.toHexString(System.identityHashCode(this)) + } + } +} + +private const val TAG = "MobileViewLogger" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt new file mode 100644 index 000000000000..f67bc8f14447 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.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.statusbar.pipeline.mobile.ui + +import android.view.View +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.statusbar.pipeline.dagger.VerboseMobileViewLog +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger.Companion.getIdForLogging +import com.android.systemui.statusbar.pipeline.mobile.ui.model.SignalIconModel +import javax.inject.Inject + +/** + * Logs for **verbose** changes with the new mobile views. + * + * This is a hopefully temporary log until we resolve some open bugs (b/267236367, b/269565345, + * b/270300839). + */ +@SysUISingleton +class VerboseMobileViewLogger +@Inject +constructor( + @VerboseMobileViewLog private val buffer: LogBuffer, +) { + fun logBinderReceivedSignalIcon(parentView: View, subId: Int, icon: SignalIconModel) { + buffer.log( + TAG, + LogLevel.VERBOSE, + { + str1 = parentView.getIdForLogging() + int1 = subId + int2 = icon.level + bool1 = icon.showExclamationMark + }, + { + "Binder[subId=$int1, viewId=$str1] received new signal icon: " + + "level=$int2 showExclamation=$bool1" + }, + ) + } + + fun logBinderReceivedNetworkTypeIcon(parentView: View, subId: Int, icon: Icon.Resource?) { + buffer.log( + TAG, + LogLevel.VERBOSE, + { + str1 = parentView.getIdForLogging() + int1 = subId + bool1 = icon != null + int2 = icon?.res ?: -1 + }, + { + "Binder[subId=$int1, viewId=$str1] received new network type icon: " + + if (bool1) "resId=$int2" else "null" + }, + ) + } +} + +private const val TAG = "VerboseMobileViewLogger" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt index db585e68d185..5b7d45b55c5c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt @@ -36,8 +36,10 @@ import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding +import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch @@ -48,6 +50,7 @@ object MobileIconBinder { fun bind( view: ViewGroup, viewModel: LocationBasedMobileViewModel, + logger: MobileViewLogger, ): ModernStatusBarViewBinding { val mobileGroupView = view.requireViewById<ViewGroup>(R.id.mobile_group) val activityContainer = view.requireViewById<View>(R.id.inout_container) @@ -70,8 +73,13 @@ object MobileIconBinder { val iconTint: MutableStateFlow<Int> = MutableStateFlow(viewModel.defaultColor) val decorTint: MutableStateFlow<Int> = MutableStateFlow(viewModel.defaultColor) + var isCollecting: Boolean = false + view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { + logger.logCollectionStarted(view, viewModel) + isCollecting = true + launch { visibilityState.collect { state -> when (state) { @@ -96,6 +104,11 @@ object MobileIconBinder { // Set the icon for the triangle launch { viewModel.icon.distinctUntilChanged().collect { icon -> + viewModel.verboseLogger?.logBinderReceivedSignalIcon( + view, + viewModel.subscriptionId, + icon, + ) mobileDrawable.level = SignalDrawable.getState( icon.level, @@ -114,6 +127,11 @@ object MobileIconBinder { // Set the network type icon launch { viewModel.networkTypeIcon.distinctUntilChanged().collect { dataTypeId -> + viewModel.verboseLogger?.logBinderReceivedNetworkTypeIcon( + view, + viewModel.subscriptionId, + dataTypeId, + ) dataTypeId?.let { IconViewBinder.bind(dataTypeId, networkTypeView) } networkTypeView.visibility = if (dataTypeId != null) VISIBLE else GONE } @@ -150,6 +168,13 @@ object MobileIconBinder { } launch { decorTint.collect { tint -> dotView.setDecorColor(tint) } } + + try { + awaitCancellation() + } finally { + isCollecting = false + logger.logCollectionStopped(view, viewModel) + } } } @@ -175,6 +200,10 @@ object MobileIconBinder { } decorTint.value = newTint } + + override fun isCollecting(): Boolean { + return isCollecting + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt index ed9a1884a7b4..4144293d5ccd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt @@ -20,6 +20,8 @@ import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import com.android.systemui.R +import com.android.systemui.statusbar.StatusBarIconView.getVisibleStateString +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinder import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView @@ -31,6 +33,15 @@ class ModernStatusBarMobileView( var subId: Int = -1 + override fun toString(): String { + return "ModernStatusBarMobileView(" + + "slot='$slot', " + + "subId=$subId, " + + "isCollecting=${binding.isCollecting()}, " + + "visibleState=${getVisibleStateString(visibleState)}); " + + "viewString=${super.toString()}" + } + companion object { /** @@ -40,6 +51,7 @@ class ModernStatusBarMobileView( @JvmStatic fun constructAndBind( context: Context, + logger: MobileViewLogger, slot: String, viewModel: LocationBasedMobileViewModel, ): ModernStatusBarMobileView { @@ -48,7 +60,8 @@ class ModernStatusBarMobileView( as ModernStatusBarMobileView) .also { it.subId = viewModel.subscriptionId - it.initView(slot) { MobileIconBinder.bind(it, viewModel) } + it.initView(slot) { MobileIconBinder.bind(it, viewModel, logger) } + logger.logNewViewBinding(it, viewModel) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt index 8e103f7bee2f..f775940140cc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel import android.graphics.Color import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger /** * A view model for an individual mobile icon that embeds the notion of a [StatusBarLocation]. This @@ -26,11 +27,15 @@ import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags * * @param commonImpl for convenience, this class wraps a base interface that can provides all of the * common implementations between locations. See [MobileIconViewModel] + * @property locationName the name of the location of this VM, used for logging. + * @property verboseLogger an optional logger to log extremely verbose view updates. */ abstract class LocationBasedMobileViewModel( val commonImpl: MobileIconViewModelCommon, statusBarPipelineFlags: StatusBarPipelineFlags, debugTint: Int, + val locationName: String, + val verboseLogger: VerboseMobileViewLogger?, ) : MobileIconViewModelCommon by commonImpl { val useDebugColoring: Boolean = statusBarPipelineFlags.useDebugColoring() @@ -45,11 +50,16 @@ abstract class LocationBasedMobileViewModel( fun viewModelForLocation( commonImpl: MobileIconViewModelCommon, statusBarPipelineFlags: StatusBarPipelineFlags, + verboseMobileViewLogger: VerboseMobileViewLogger, loc: StatusBarLocation, ): LocationBasedMobileViewModel = when (loc) { StatusBarLocation.HOME -> - HomeMobileIconViewModel(commonImpl, statusBarPipelineFlags) + HomeMobileIconViewModel( + commonImpl, + statusBarPipelineFlags, + verboseMobileViewLogger, + ) StatusBarLocation.KEYGUARD -> KeyguardMobileIconViewModel(commonImpl, statusBarPipelineFlags) StatusBarLocation.QS -> QsMobileIconViewModel(commonImpl, statusBarPipelineFlags) @@ -60,20 +70,41 @@ abstract class LocationBasedMobileViewModel( class HomeMobileIconViewModel( commonImpl: MobileIconViewModelCommon, statusBarPipelineFlags: StatusBarPipelineFlags, + verboseMobileViewLogger: VerboseMobileViewLogger, ) : MobileIconViewModelCommon, - LocationBasedMobileViewModel(commonImpl, statusBarPipelineFlags, debugTint = Color.CYAN) + LocationBasedMobileViewModel( + commonImpl, + statusBarPipelineFlags, + debugTint = Color.CYAN, + locationName = "Home", + verboseMobileViewLogger, + ) class QsMobileIconViewModel( commonImpl: MobileIconViewModelCommon, statusBarPipelineFlags: StatusBarPipelineFlags, ) : MobileIconViewModelCommon, - LocationBasedMobileViewModel(commonImpl, statusBarPipelineFlags, debugTint = Color.GREEN) + LocationBasedMobileViewModel( + commonImpl, + statusBarPipelineFlags, + debugTint = Color.GREEN, + locationName = "QS", + // Only do verbose logging for the Home location. + verboseLogger = null, + ) class KeyguardMobileIconViewModel( commonImpl: MobileIconViewModelCommon, statusBarPipelineFlags: StatusBarPipelineFlags, ) : MobileIconViewModelCommon, - LocationBasedMobileViewModel(commonImpl, statusBarPipelineFlags, debugTint = Color.MAGENTA) + LocationBasedMobileViewModel( + commonImpl, + statusBarPipelineFlags, + debugTint = Color.MAGENTA, + locationName = "Keyguard", + // Only do verbose logging for the Home location. + verboseLogger = null, + ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt index 049627899eff..dbb534b24471 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt @@ -49,7 +49,7 @@ interface MobileIconViewModelCommon { val contentDescription: Flow<ContentDescription> val roaming: Flow<Boolean> /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */ - val networkTypeIcon: Flow<Icon?> + val networkTypeIcon: Flow<Icon.Resource?> val activityInVisible: Flow<Boolean> val activityOutVisible: Flow<Boolean> val activityContainerVisible: Flow<Boolean> @@ -161,7 +161,7 @@ constructor( ) .stateIn(scope, SharingStarted.WhileSubscribed(), false) - override val networkTypeIcon: Flow<Icon?> = + override val networkTypeIcon: Flow<Icon.Resource?> = combine( iconInteractor.networkTypeIconGroup, showNetworkTypeIcon, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt index 8cb52af336da..2b90065284d0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt @@ -23,6 +23,8 @@ import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger +import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import javax.inject.Inject @@ -39,6 +41,8 @@ class MobileIconsViewModel @Inject constructor( val subscriptionIdsFlow: StateFlow<List<Int>>, + val logger: MobileViewLogger, + private val verboseLogger: VerboseMobileViewLogger, private val interactor: MobileIconsInteractor, private val airplaneModeInteractor: AirplaneModeInteractor, private val constants: ConnectivityConstants, @@ -66,6 +70,7 @@ constructor( return LocationBasedMobileViewModel.viewModelForLocation( common, statusBarPipelineFlags, + verboseLogger, location, ) } @@ -79,6 +84,8 @@ constructor( class Factory @Inject constructor( + private val logger: MobileViewLogger, + private val verboseLogger: VerboseMobileViewLogger, private val interactor: MobileIconsInteractor, private val airplaneModeInteractor: AirplaneModeInteractor, private val constants: ConnectivityConstants, @@ -88,6 +95,8 @@ constructor( fun create(subscriptionIdsFlow: StateFlow<List<Int>>): MobileIconsViewModel { return MobileIconsViewModel( subscriptionIdsFlow, + logger, + verboseLogger, interactor, airplaneModeInteractor, constants, 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/shared/ui/binder/ModernStatusBarViewBinding.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/ModernStatusBarViewBinding.kt index f67876b50233..81f8683411ca 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/ModernStatusBarViewBinding.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/ModernStatusBarViewBinding.kt @@ -37,4 +37,7 @@ interface ModernStatusBarViewBinding { /** Notifies that the decor tint has been updated (used only for the dot). */ fun onDecorTintChanged(newTint: Int) + + /** Returns true if the binding between the view and view-model is currently collecting. */ + fun isCollecting(): Boolean } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt index b1e28129a690..1a1340484bfc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt @@ -36,7 +36,7 @@ open class ModernStatusBarView(context: Context, attrs: AttributeSet?) : BaseStatusBarFrameLayout(context, attrs) { private lateinit var slot: String - private lateinit var binding: ModernStatusBarViewBinding + internal lateinit var binding: ModernStatusBarViewBinding @StatusBarIconView.VisibleState private var iconVisibleState: Int = STATE_HIDDEN diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt index ee58160a7d3b..b5e7b7a13505 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt @@ -128,6 +128,7 @@ constructor( } override fun onLost(network: Network) { + logger.logOnLost(network, isDefaultNetworkCallback = true) // The system no longer has a default network, so wifi is definitely not // default. trySend(false) @@ -179,7 +180,7 @@ constructor( } override fun onLost(network: Network) { - logger.logOnLost(network) + logger.logOnLost(network, isDefaultNetworkCallback = false) wifiNetworkChangeEvents.tryEmit(Unit) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiInputLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiInputLogger.kt index a32e47592355..bb0b166f7aba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiInputLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiInputLogger.kt @@ -48,8 +48,8 @@ constructor( ) } - fun logOnLost(network: Network) { - LoggerHelper.logOnLost(buffer, TAG, network) + fun logOnLost(network: Network, isDefaultNetworkCallback: Boolean) { + LoggerHelper.logOnLost(buffer, TAG, network, isDefaultNetworkCallback) } fun logIntent(intentName: String) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt index 2aff12c8721d..9e8c814ca2a9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt @@ -34,6 +34,7 @@ import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarV import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.distinctUntilChanged @@ -74,8 +75,12 @@ object WifiViewBinder { val iconTint: MutableStateFlow<Int> = MutableStateFlow(viewModel.defaultColor) val decorTint: MutableStateFlow<Int> = MutableStateFlow(viewModel.defaultColor) + var isCollecting: Boolean = false + view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { + isCollecting = true + launch { visibilityState.collect { visibilityState -> groupView.isVisible = visibilityState == STATE_ICON @@ -127,6 +132,12 @@ object WifiViewBinder { airplaneSpacer.isVisible = visible } } + + try { + awaitCancellation() + } finally { + isCollecting = false + } } } @@ -152,6 +163,10 @@ object WifiViewBinder { } decorTint.value = newTint } + + override fun isCollecting(): Boolean { + return isCollecting + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt index 7a734862fe1b..f23e10287164 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt @@ -21,6 +21,7 @@ import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import com.android.systemui.R +import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView import com.android.systemui.statusbar.pipeline.wifi.ui.binder.WifiViewBinder import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel @@ -33,6 +34,15 @@ class ModernStatusBarWifiView( context: Context, attrs: AttributeSet?, ) : ModernStatusBarView(context, attrs) { + + override fun toString(): String { + return "ModernStatusBarWifiView(" + + "slot='$slot', " + + "isCollecting=${binding.isCollecting()}, " + + "visibleState=${StatusBarIconView.getVisibleStateString(visibleState)}); " + + "viewString=${super.toString()}" + } + companion object { /** * Inflates a new instance of [ModernStatusBarWifiView], binds it to a view model, and @@ -45,12 +55,9 @@ class ModernStatusBarWifiView( slot: String, wifiViewModel: LocationBasedWifiViewModel, ): ModernStatusBarWifiView { - return ( - LayoutInflater.from(context).inflate(R.layout.new_status_bar_wifi_group, null) - as ModernStatusBarWifiView - ).also { - it.initView(slot) { WifiViewBinder.bind(it, wifiViewModel) } - } + return (LayoutInflater.from(context).inflate(R.layout.new_status_bar_wifi_group, null) + as ModernStatusBarWifiView) + .also { it.initView(slot) { WifiViewBinder.bind(it, wifiViewModel) } } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java index 7acdaffb48c4..01fabcc8bc1e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java @@ -22,13 +22,18 @@ import static android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_LOCKED import android.annotation.Nullable; import android.hardware.devicestate.DeviceStateManager; import android.os.Trace; -import android.util.Log; +import android.util.IndentingPrintWriter; + +import androidx.annotation.NonNull; import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManager; +import com.android.systemui.Dumpable; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.dump.DumpManager; import com.android.systemui.util.wrapper.RotationPolicyWrapper; +import java.io.PrintWriter; import java.util.concurrent.Executor; import javax.inject.Inject; @@ -39,19 +44,19 @@ import javax.inject.Inject; */ @SysUISingleton public final class DeviceStateRotationLockSettingController - implements Listenable, RotationLockController.RotationLockControllerCallback { - - private static final String TAG = "DSRotateLockSettingCon"; + implements Listenable, RotationLockController.RotationLockControllerCallback, Dumpable { private final RotationPolicyWrapper mRotationPolicyWrapper; private final DeviceStateManager mDeviceStateManager; private final Executor mMainExecutor; private final DeviceStateRotationLockSettingsManager mDeviceStateRotationLockSettingsManager; + private final DeviceStateRotationLockSettingControllerLogger mLogger; // On registration for DeviceStateCallback, we will receive a callback with the current state // and this will be initialized. private int mDeviceState = -1; - @Nullable private DeviceStateManager.DeviceStateCallback mDeviceStateCallback; + @Nullable + private DeviceStateManager.DeviceStateCallback mDeviceStateCallback; private DeviceStateRotationLockSettingsManager.DeviceStateRotationLockSettingsListener mDeviceStateRotationLockSettingsListener; @@ -60,21 +65,27 @@ public final class DeviceStateRotationLockSettingController RotationPolicyWrapper rotationPolicyWrapper, DeviceStateManager deviceStateManager, @Main Executor executor, - DeviceStateRotationLockSettingsManager deviceStateRotationLockSettingsManager) { + DeviceStateRotationLockSettingsManager deviceStateRotationLockSettingsManager, + DeviceStateRotationLockSettingControllerLogger logger, + DumpManager dumpManager) { mRotationPolicyWrapper = rotationPolicyWrapper; mDeviceStateManager = deviceStateManager; mMainExecutor = executor; mDeviceStateRotationLockSettingsManager = deviceStateRotationLockSettingsManager; + mLogger = logger; + dumpManager.registerDumpable(this); } @Override public void setListening(boolean listening) { + mLogger.logListeningChange(listening); if (listening) { // Note that this is called once with the initial state of the device, even if there // is no user action. mDeviceStateCallback = this::updateDeviceState; mDeviceStateManager.registerCallback(mMainExecutor, mDeviceStateCallback); - mDeviceStateRotationLockSettingsListener = () -> readPersistedSetting(mDeviceState); + mDeviceStateRotationLockSettingsListener = () -> + readPersistedSetting("deviceStateRotationLockChange", mDeviceState); mDeviceStateRotationLockSettingsManager.registerListener( mDeviceStateRotationLockSettingsListener); } else { @@ -89,35 +100,28 @@ public final class DeviceStateRotationLockSettingController } @Override - public void onRotationLockStateChanged(boolean rotationLocked, boolean affordanceVisible) { - if (mDeviceState == -1) { - Log.wtf(TAG, "Device state was not initialized."); + public void onRotationLockStateChanged(boolean newRotationLocked, boolean affordanceVisible) { + int deviceState = mDeviceState; + boolean currentRotationLocked = mDeviceStateRotationLockSettingsManager + .isRotationLocked(deviceState); + mLogger.logRotationLockStateChanged(deviceState, newRotationLocked, currentRotationLocked); + if (deviceState == -1) { return; } - - if (rotationLocked - == mDeviceStateRotationLockSettingsManager.isRotationLocked(mDeviceState)) { - Log.v(TAG, "Rotation lock same as the current setting, no need to update."); + if (newRotationLocked == currentRotationLocked) { return; } - - saveNewRotationLockSetting(rotationLocked); + saveNewRotationLockSetting(newRotationLocked); } private void saveNewRotationLockSetting(boolean isRotationLocked) { - Log.v( - TAG, - "saveNewRotationLockSetting [state=" - + mDeviceState - + "] [isRotationLocked=" - + isRotationLocked - + "]"); - - mDeviceStateRotationLockSettingsManager.updateSetting(mDeviceState, isRotationLocked); + int deviceState = mDeviceState; + mLogger.logSaveNewRotationLockSetting(isRotationLocked, deviceState); + mDeviceStateRotationLockSettingsManager.updateSetting(deviceState, isRotationLocked); } private void updateDeviceState(int state) { - Log.v(TAG, "updateDeviceState [state=" + state + "]"); + mLogger.logUpdateDeviceState(mDeviceState, state); if (Trace.isEnabled()) { Trace.traceBegin( Trace.TRACE_TAG_APP, "updateDeviceState [state=" + state + "]"); @@ -127,22 +131,26 @@ public final class DeviceStateRotationLockSettingController return; } - readPersistedSetting(state); + readPersistedSetting("updateDeviceState", state); } finally { Trace.endSection(); } } - private void readPersistedSetting(int state) { + private void readPersistedSetting(String caller, int state) { int rotationLockSetting = mDeviceStateRotationLockSettingsManager.getRotationLockSetting(state); + boolean shouldBeLocked = rotationLockSetting == DEVICE_STATE_ROTATION_LOCK_LOCKED; + boolean isLocked = mRotationPolicyWrapper.isRotationLocked(); + + mLogger.readPersistedSetting(caller, state, rotationLockSetting, shouldBeLocked, isLocked); + if (rotationLockSetting == DEVICE_STATE_ROTATION_LOCK_IGNORED) { // This should not happen. Device states that have an ignored setting, should also // specify a fallback device state which is not ignored. // We won't handle this device state. The same rotation lock setting as before should // apply and any changes to the rotation lock setting will be written for the previous // valid device state. - Log.w(TAG, "Missing fallback. Ignoring new device state: " + state); return; } @@ -150,9 +158,18 @@ public final class DeviceStateRotationLockSettingController mDeviceState = state; // Update the rotation policy, if needed, for this new device state - boolean newRotationLockSetting = rotationLockSetting == DEVICE_STATE_ROTATION_LOCK_LOCKED; - if (newRotationLockSetting != mRotationPolicyWrapper.isRotationLocked()) { - mRotationPolicyWrapper.setRotationLock(newRotationLockSetting); + if (shouldBeLocked != isLocked) { + mRotationPolicyWrapper.setRotationLock(shouldBeLocked); } } + + @Override + public void dump(@NonNull PrintWriter printWriter, @NonNull String[] args) { + IndentingPrintWriter pw = new IndentingPrintWriter(printWriter); + mDeviceStateRotationLockSettingsManager.dump(pw); + pw.println("DeviceStateRotationLockSettingController"); + pw.increaseIndent(); + pw.println("mDeviceState: " + mDeviceState); + pw.decreaseIndent(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerLogger.kt new file mode 100644 index 000000000000..aa502bc48149 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerLogger.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.statusbar.policy + +import android.content.Context +import android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_IGNORED +import android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_LOCKED +import android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_UNLOCKED +import com.android.internal.R +import com.android.systemui.log.dagger.DeviceStateAutoRotationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.VERBOSE +import javax.inject.Inject + +class DeviceStateRotationLockSettingControllerLogger +@Inject +constructor(@DeviceStateAutoRotationLog private val logBuffer: LogBuffer, context: Context) { + + private val foldedStates = context.resources.getIntArray(R.array.config_foldedDeviceStates) + private val halfFoldedStates = + context.resources.getIntArray(R.array.config_halfFoldedDeviceStates) + private val unfoldedStates = context.resources.getIntArray(R.array.config_openDeviceStates) + + fun logListeningChange(listening: Boolean) { + logBuffer.log(TAG, VERBOSE, { bool1 = listening }, { "setListening: $bool1" }) + } + + fun logRotationLockStateChanged( + state: Int, + newRotationLocked: Boolean, + currentRotationLocked: Boolean + ) { + logBuffer.log( + TAG, + VERBOSE, + { + int1 = state + bool1 = newRotationLocked + bool2 = currentRotationLocked + }, + { + "onRotationLockStateChanged: " + + "state=$int1 [${int1.toDevicePostureString()}], " + + "newRotationLocked=$bool1, " + + "currentRotationLocked=$bool2" + } + ) + } + + fun logSaveNewRotationLockSetting(isRotationLocked: Boolean, state: Int) { + logBuffer.log( + TAG, + VERBOSE, + { + bool1 = isRotationLocked + int1 = state + }, + { "saveNewRotationLockSetting: isRotationLocked=$bool1, state=$int1" } + ) + } + + fun logUpdateDeviceState(currentState: Int, newState: Int) { + logBuffer.log( + TAG, + VERBOSE, + { + int1 = currentState + int2 = newState + }, + { + "updateDeviceState: " + + "current=$int1 [${int1.toDevicePostureString()}], " + + "new=$int2 [${int2.toDevicePostureString()}]" + } + ) + } + + fun readPersistedSetting( + caller: String, + state: Int, + rotationLockSetting: Int, + shouldBeLocked: Boolean, + isLocked: Boolean + ) { + logBuffer.log( + TAG, + VERBOSE, + { + str1 = caller + int1 = state + int2 = rotationLockSetting + bool1 = shouldBeLocked + bool2 = isLocked + }, + { + "readPersistedSetting: " + + "caller=$str1, " + + "state=$int1 [${int1.toDevicePostureString()}], " + + "rotationLockSettingForState: ${int2.toRotationLockSettingString()}, " + + "shouldBeLocked=$bool1, " + + "isLocked=$bool2" + } + ) + } + + private fun Int.toDevicePostureString(): String { + return when (this) { + in foldedStates -> "Folded" + in unfoldedStates -> "Unfolded" + in halfFoldedStates -> "Half-Folded" + -1 -> "Uninitialized" + else -> "Unknown" + } + } +} + +private fun Int.toRotationLockSettingString(): String { + return when (this) { + DEVICE_STATE_ROTATION_LOCK_IGNORED -> "IGNORED" + DEVICE_STATE_ROTATION_LOCK_LOCKED -> "LOCKED" + DEVICE_STATE_ROTATION_LOCK_UNLOCKED -> "UNLOCKED" + else -> "Unknown" + } +} + +private const val TAG = "DSRotateLockSettingCon" diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt index 3f895ad0b5b4..02d447976746 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt @@ -32,6 +32,8 @@ import android.os.UserManager import android.provider.Settings import android.util.Log import com.android.internal.util.UserIcons +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.R import com.android.systemui.SystemUISecondaryUserService import com.android.systemui.animation.Expandable @@ -90,6 +92,7 @@ constructor( @Application private val applicationScope: CoroutineScope, telephonyInteractor: TelephonyInteractor, broadcastDispatcher: BroadcastDispatcher, + keyguardUpdateMonitor: KeyguardUpdateMonitor, @Background private val backgroundDispatcher: CoroutineDispatcher, private val activityManager: ActivityManager, private val refreshUsersScheduler: RefreshUsersScheduler, @@ -286,6 +289,12 @@ constructor( val isSimpleUserSwitcher: Boolean get() = repository.isSimpleUserSwitcher() + val keyguardUpdateMonitorCallback = + object : KeyguardUpdateMonitorCallback() { + override fun onKeyguardGoingAway() { + dismissDialog() + } + } init { refreshUsersScheduler.refreshIfNotPaused() @@ -316,6 +325,7 @@ constructor( onBroadcastReceived(intent, previousSelectedUser) } .launchIn(applicationScope) + keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) } fun addCallback(callback: UserCallback) { diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java index 81ae6e851fb9..c72853ef37be 100644 --- a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java @@ -115,6 +115,17 @@ public abstract class SysUIConcurrencyModule { } /** + * Provide a Long running Executor. + */ + @Provides + @SysUISingleton + @LongRunning + public static DelayableExecutor provideLongRunningDelayableExecutor( + @LongRunning Looper looper) { + return new ExecutorImpl(looper); + } + + /** * Provide a Background-Thread Executor. */ @Provides 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/src/com/android/systemui/wallpapers/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java index 8b925b7ad312..b962148b79dc 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java @@ -35,7 +35,7 @@ import android.view.WindowManager; import androidx.annotation.NonNull; import com.android.internal.annotations.VisibleForTesting; -import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.LongRunning; import com.android.systemui.settings.UserTracker; import com.android.systemui.util.concurrency.DelayableExecutor; @@ -61,17 +61,16 @@ public class ImageWallpaper extends WallpaperService { private final UserTracker mUserTracker; // used for most tasks (call canvas.drawBitmap, load/unload the bitmap) - @Background - private final DelayableExecutor mBackgroundExecutor; + @LongRunning + private final DelayableExecutor mLongExecutor; // wait at least this duration before unloading the bitmap private static final int DELAY_UNLOAD_BITMAP = 2000; @Inject - public ImageWallpaper(@Background DelayableExecutor backgroundExecutor, - UserTracker userTracker) { + public ImageWallpaper(@LongRunning DelayableExecutor longExecutor, UserTracker userTracker) { super(); - mBackgroundExecutor = backgroundExecutor; + mLongExecutor = longExecutor; mUserTracker = userTracker; } @@ -105,7 +104,7 @@ public class ImageWallpaper extends WallpaperService { setFixedSizeAllowed(true); setShowForAllUsers(true); mWallpaperLocalColorExtractor = new WallpaperLocalColorExtractor( - mBackgroundExecutor, + mLongExecutor, new WallpaperLocalColorExtractor.WallpaperLocalColorExtractorCallback() { @Override public void onColorsProcessed(List<RectF> regions, @@ -202,7 +201,7 @@ public class ImageWallpaper extends WallpaperService { } private void drawFrame() { - mBackgroundExecutor.execute(this::drawFrameSynchronized); + mLongExecutor.execute(this::drawFrameSynchronized); } private void drawFrameSynchronized() { @@ -257,7 +256,7 @@ public class ImageWallpaper extends WallpaperService { } private void unloadBitmapIfNotUsed() { - mBackgroundExecutor.execute(this::unloadBitmapIfNotUsedSynchronized); + mLongExecutor.execute(this::unloadBitmapIfNotUsedSynchronized); } private void unloadBitmapIfNotUsedSynchronized() { @@ -341,7 +340,7 @@ public class ImageWallpaper extends WallpaperService { * - the mini bitmap from color extractor is recomputed * - the DELAY_UNLOAD_BITMAP has passed */ - mBackgroundExecutor.executeDelayed( + mLongExecutor.executeDelayed( this::unloadBitmapIfNotUsedSynchronized, DELAY_UNLOAD_BITMAP); } // even if the bitmap cannot be loaded, call reportEngineShown diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/WallpaperLocalColorExtractor.java b/packages/SystemUI/src/com/android/systemui/wallpapers/WallpaperLocalColorExtractor.java index 988fd710d2dc..1e8446f8df1d 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/WallpaperLocalColorExtractor.java +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/WallpaperLocalColorExtractor.java @@ -29,7 +29,7 @@ import android.util.MathUtils; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; -import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.LongRunning; import com.android.systemui.util.Assert; import java.io.FileDescriptor; @@ -66,8 +66,8 @@ public class WallpaperLocalColorExtractor { private final List<RectF> mPendingRegions = new ArrayList<>(); private final Set<RectF> mProcessedRegions = new ArraySet<>(); - @Background - private final Executor mBackgroundExecutor; + @LongRunning + private final Executor mLongExecutor; private final WallpaperLocalColorExtractorCallback mWallpaperLocalColorExtractorCallback; @@ -101,13 +101,13 @@ public class WallpaperLocalColorExtractor { /** * Creates a new color extractor. - * @param backgroundExecutor the executor on which the color extraction will be performed + * @param longExecutor the executor on which the color extraction will be performed * @param wallpaperLocalColorExtractorCallback an interface to handle the callbacks from * the color extractor. */ - public WallpaperLocalColorExtractor(@Background Executor backgroundExecutor, + public WallpaperLocalColorExtractor(@LongRunning Executor longExecutor, WallpaperLocalColorExtractorCallback wallpaperLocalColorExtractorCallback) { - mBackgroundExecutor = backgroundExecutor; + mLongExecutor = longExecutor; mWallpaperLocalColorExtractorCallback = wallpaperLocalColorExtractorCallback; } @@ -117,7 +117,7 @@ public class WallpaperLocalColorExtractor { * not recomputed. */ public void setDisplayDimensions(int displayWidth, int displayHeight) { - mBackgroundExecutor.execute(() -> + mLongExecutor.execute(() -> setDisplayDimensionsSynchronized(displayWidth, displayHeight)); } @@ -144,7 +144,7 @@ public class WallpaperLocalColorExtractor { * @param bitmap the new wallpaper */ public void onBitmapChanged(@NonNull Bitmap bitmap) { - mBackgroundExecutor.execute(() -> onBitmapChangedSynchronized(bitmap)); + mLongExecutor.execute(() -> onBitmapChangedSynchronized(bitmap)); } private void onBitmapChangedSynchronized(@NonNull Bitmap bitmap) { @@ -167,7 +167,7 @@ public class WallpaperLocalColorExtractor { * @param pages the total number of pages of the launcher */ public void onPageChanged(int pages) { - mBackgroundExecutor.execute(() -> onPageChangedSynchronized(pages)); + mLongExecutor.execute(() -> onPageChangedSynchronized(pages)); } private void onPageChangedSynchronized(int pages) { @@ -194,7 +194,7 @@ public class WallpaperLocalColorExtractor { */ public void addLocalColorsAreas(@NonNull List<RectF> regions) { if (regions.size() > 0) { - mBackgroundExecutor.execute(() -> addLocalColorsAreasSynchronized(regions)); + mLongExecutor.execute(() -> addLocalColorsAreasSynchronized(regions)); } else { Log.w(TAG, "Attempt to add colors with an empty list"); } @@ -218,7 +218,7 @@ public class WallpaperLocalColorExtractor { * @param regions The areas of interest in our wallpaper (in screen pixel coordinates) */ public void removeLocalColorAreas(@NonNull List<RectF> regions) { - mBackgroundExecutor.execute(() -> removeLocalColorAreasSynchronized(regions)); + mLongExecutor.execute(() -> removeLocalColorAreasSynchronized(regions)); } private void removeLocalColorAreasSynchronized(@NonNull List<RectF> regions) { @@ -236,7 +236,7 @@ public class WallpaperLocalColorExtractor { * Clean up the memory (in particular, the mini bitmap) used by this class. */ public void cleanUp() { - mBackgroundExecutor.execute(this::cleanUpSynchronized); + mLongExecutor.execute(this::cleanUpSynchronized); } private void cleanUpSynchronized() { diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java index 5d896cbbdab4..56df3e114c0b 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java @@ -25,7 +25,6 @@ import static android.service.notification.NotificationListenerService.REASON_GR import static android.service.notification.NotificationStats.DISMISSAL_BUBBLE; import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL; -import static com.android.systemui.flags.Flags.WM_BUBBLE_BAR; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; @@ -358,7 +357,6 @@ public class BubblesManager { }); } }; - mBubbles.setBubbleBarEnabled(featureFlags.isEnabled(WM_BUBBLE_BAR)); mBubbles.setSysuiProxy(mSysuiProxy); } 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/biometrics/AuthRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt index b765ab3c5eac..a245c01d74de 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt @@ -25,7 +25,9 @@ import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback +import com.android.keyguard.logging.KeyguardLogger import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.logcatLogBuffer import com.android.systemui.flags.FeatureFlags import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.plugins.statusbar.StatusBarStateController @@ -109,6 +111,7 @@ class AuthRippleControllerTest : SysuiTestCase() { udfpsControllerProvider, statusBarStateController, featureFlags, + KeyguardLogger(logcatLogBuffer(AuthRippleController.TAG)), rippleView ) controller.init() diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogTest.java index 35039026fe9e..9d16185e75c5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogTest.java @@ -45,8 +45,8 @@ import org.mockito.MockitoAnnotations; public class BroadcastDialogTest extends SysuiTestCase { private static final String CURRENT_BROADCAST_APP = "Music"; - private static final String SWITCH_APP = "Files by Google"; - private static final String TEST_PACKAGE = "com.google.android.apps.nbu.files"; + private static final String SWITCH_APP = "System UI"; + private static final String TEST_PACKAGE = "com.android.systemui"; private BroadcastDialog mBroadcastDialog; private View mDialogView; private TextView mTitle; @@ -59,6 +59,7 @@ public class BroadcastDialogTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); mBroadcastDialog = new BroadcastDialog(mContext, mock(MediaOutputDialogFactory.class), CURRENT_BROADCAST_APP, TEST_PACKAGE, mock(UiEventLogger.class)); + mBroadcastDialog.show(); mDialogView = mBroadcastDialog.mDialogView; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/DistanceClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/DistanceClassifierTest.java index faa5db4327a6..ab6d5b771d5a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/DistanceClassifierTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/DistanceClassifierTest.java @@ -94,7 +94,9 @@ public class DistanceClassifierTest extends ClassifierTest { mClassifier.onTouchEvent(appendMoveEvent(1, 16, 3)); mClassifier.onTouchEvent(appendMoveEvent(1, 17, 300)); mClassifier.onTouchEvent(appendMoveEvent(1, 18, 301)); - mClassifier.onTouchEvent(appendUpEvent(1, 19, 501)); + mClassifier.onTouchEvent(appendMoveEvent(1, 19, 501)); + mClassifier.onTouchEvent(appendUpEvent(1, 19, 501)); //event will be dropped + assertThat(mClassifier.classifyGesture(0, 0.5, 1).isFalse()).isTrue(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java index 2edc3d361316..8eadadff1ca5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java @@ -75,16 +75,17 @@ public class FalsingDataProviderTest extends ClassifierTest { } @Test - public void test_trackMotionEvents() { + public void test_trackMotionEvents_dropUpEvent() { mDataProvider.onMotionEvent(appendDownEvent(2, 9)); mDataProvider.onMotionEvent(appendMoveEvent(4, 7)); - mDataProvider.onMotionEvent(appendUpEvent(6, 5)); + mDataProvider.onMotionEvent(appendMoveEvent(6, 5)); + mDataProvider.onMotionEvent(appendUpEvent(0, 0)); // event will be dropped List<MotionEvent> motionEventList = mDataProvider.getRecentMotionEvents(); assertThat(motionEventList.size()).isEqualTo(3); assertThat(motionEventList.get(0).getActionMasked()).isEqualTo(MotionEvent.ACTION_DOWN); assertThat(motionEventList.get(1).getActionMasked()).isEqualTo(MotionEvent.ACTION_MOVE); - assertThat(motionEventList.get(2).getActionMasked()).isEqualTo(MotionEvent.ACTION_UP); + assertThat(motionEventList.get(2).getActionMasked()).isEqualTo(MotionEvent.ACTION_MOVE); assertThat(motionEventList.get(0).getEventTime()).isEqualTo(1L); assertThat(motionEventList.get(1).getEventTime()).isEqualTo(2L); assertThat(motionEventList.get(2).getEventTime()).isEqualTo(3L); @@ -97,6 +98,28 @@ public class FalsingDataProviderTest extends ClassifierTest { } @Test + public void test_trackMotionEvents_keepUpEvent() { + mDataProvider.onMotionEvent(appendDownEvent(2, 9)); + mDataProvider.onMotionEvent(appendMoveEvent(4, 7)); + mDataProvider.onMotionEvent(appendUpEvent(0, 0, 100)); + List<MotionEvent> motionEventList = mDataProvider.getRecentMotionEvents(); + + assertThat(motionEventList.size()).isEqualTo(3); + assertThat(motionEventList.get(0).getActionMasked()).isEqualTo(MotionEvent.ACTION_DOWN); + assertThat(motionEventList.get(1).getActionMasked()).isEqualTo(MotionEvent.ACTION_MOVE); + assertThat(motionEventList.get(2).getActionMasked()).isEqualTo(MotionEvent.ACTION_UP); + assertThat(motionEventList.get(0).getEventTime()).isEqualTo(1L); + assertThat(motionEventList.get(1).getEventTime()).isEqualTo(2L); + assertThat(motionEventList.get(2).getEventTime()).isEqualTo(100); + assertThat(motionEventList.get(0).getX()).isEqualTo(2f); + assertThat(motionEventList.get(1).getX()).isEqualTo(4f); + assertThat(motionEventList.get(2).getX()).isEqualTo(0f); + assertThat(motionEventList.get(0).getY()).isEqualTo(9f); + assertThat(motionEventList.get(1).getY()).isEqualTo(7f); + assertThat(motionEventList.get(2).getY()).isEqualTo(0f); + } + + @Test public void test_trackRecentMotionEvents() { mDataProvider.onMotionEvent(appendDownEvent(2, 9, 1)); mDataProvider.onMotionEvent(appendMoveEvent(4, 7, 800)); diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/ZigZagClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/ZigZagClassifierTest.java index c343c20398e9..ae2b8bbb4ce6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/ZigZagClassifierTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/ZigZagClassifierTest.java @@ -68,6 +68,15 @@ public class ZigZagClassifierTest extends ClassifierTest { } @Test + public void testPass_dropClosingUpEvent() { + appendMoveEvent(0, 0); + appendMoveEvent(0, 100); + appendMoveEvent(0, 200); + appendUpEvent(0, 180); // this event would push us over the maxDevianceY + assertThat(mClassifier.classifyGesture(0, 0.5, 1).isFalse()).isFalse(); + } + + @Test public void testPass_fewTouchesHorizontal() { assertThat(mClassifier.classifyGesture(0, 0.5, 1).isFalse()).isFalse(); appendMoveEvent(0, 0); diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java index 2099281d694a..ffd75fb9bbc6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java @@ -35,6 +35,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.app.RemoteAction; import android.content.ClipData; import android.content.ClipDescription; @@ -101,6 +102,9 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { private ArgumentCaptor<ClipboardOverlayView.ClipboardOverlayCallbacks> mOverlayCallbacksCaptor; private ClipboardOverlayView.ClipboardOverlayCallbacks mCallbacks; + @Captor + private ArgumentCaptor<AnimatorListenerAdapter> mAnimatorArgumentCaptor; + private FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock()); @Before @@ -446,7 +450,7 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, true); when(mClipboardUtils.isRemoteCopy(any(Context.class), any(ClipData.class), anyString())) .thenReturn(true); - when(mClipboardUtils.getAction(any(CharSequence.class), any(TextLinks.class), anyString())) + when(mClipboardUtils.getAction(any(TextLinks.class), anyString())) .thenReturn(Optional.of(Mockito.mock(RemoteAction.class))); when(mClipboardOverlayView.post(any(Runnable.class))).thenAnswer(new Answer<Object>() { @Override @@ -478,12 +482,16 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { when(mClipboardOverlayWindow.getWindowInsets()).thenReturn( getImeInsets(new Rect(0, 0, 0, 1))); mOverlayController.setClipData(mSampleClipData, ""); + Animator mockFadeoutAnimator = Mockito.mock(Animator.class); + when(mClipboardOverlayView.getMinimizedFadeoutAnimation()).thenReturn(mockFadeoutAnimator); verify(mClipboardOverlayView).setMinimized(true); verify(mClipboardOverlayView, never()).setMinimized(false); verify(mClipboardOverlayView, never()).showTextPreview(any(), anyBoolean()); mCallbacks.onMinimizedViewTapped(); + verify(mockFadeoutAnimator).addListener(mAnimatorArgumentCaptor.capture()); + mAnimatorArgumentCaptor.getValue().onAnimationEnd(mockFadeoutAnimator); verify(mClipboardOverlayView).setMinimized(false); verify(mClipboardOverlayView).showTextPreview("Test Item", false); diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtilsTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtilsTest.java index aea6be3d468b..3d8f04e08825 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtilsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtilsTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.when; @@ -77,6 +78,74 @@ public class ClipboardOverlayUtilsTest extends SysuiTestCase { @Test public void test_getAction_noLinks_returnsEmptyOptional() { + Optional<RemoteAction> action = + mClipboardUtils.getAction(Mockito.mock(TextLinks.class), "abc"); + + assertTrue(action.isEmpty()); + } + + @Test + public void test_getAction_returnsFirstLink() { + TextLinks links = getFakeTextLinksBuilder().build(); + RemoteAction actionA = constructRemoteAction("abc"); + RemoteAction actionB = constructRemoteAction("def"); + TextClassification classificationA = Mockito.mock(TextClassification.class); + when(classificationA.getActions()).thenReturn(Lists.newArrayList(actionA)); + TextClassification classificationB = Mockito.mock(TextClassification.class); + when(classificationB.getActions()).thenReturn(Lists.newArrayList(actionB)); + when(mTextClassifier.classifyText(anyString(), anyInt(), anyInt(), isNull())).thenReturn( + classificationA, classificationB); + + RemoteAction result = mClipboardUtils.getAction(links, "test").orElse(null); + + assertEquals(actionA, result); + } + + @Test + public void test_getAction_skipsMatchingComponent() { + TextLinks links = getFakeTextLinksBuilder().build(); + RemoteAction actionA = constructRemoteAction("abc"); + RemoteAction actionB = constructRemoteAction("def"); + TextClassification classificationA = Mockito.mock(TextClassification.class); + when(classificationA.getActions()).thenReturn(Lists.newArrayList(actionA)); + TextClassification classificationB = Mockito.mock(TextClassification.class); + when(classificationB.getActions()).thenReturn(Lists.newArrayList(actionB)); + when(mTextClassifier.classifyText(anyString(), anyInt(), anyInt(), isNull())).thenReturn( + classificationA, classificationB); + + RemoteAction result = mClipboardUtils.getAction(links, "abc").orElse(null); + + assertEquals(actionB, result); + } + + @Test + public void test_getAction_skipsShortEntity() { + TextLinks.Builder textLinks = new TextLinks.Builder("test text of length 22"); + final Map<String, Float> scores = new ArrayMap<>(); + scores.put(TextClassifier.TYPE_EMAIL, 1f); + textLinks.addLink(20, 22, scores); + textLinks.addLink(0, 22, scores); + + RemoteAction actionA = constructRemoteAction("abc"); + RemoteAction actionB = constructRemoteAction("def"); + TextClassification classificationA = Mockito.mock(TextClassification.class); + when(classificationA.getActions()).thenReturn(Lists.newArrayList(actionA)); + TextClassification classificationB = Mockito.mock(TextClassification.class); + when(classificationB.getActions()).thenReturn(Lists.newArrayList(actionB)); + when(mTextClassifier.classifyText(anyString(), eq(20), eq(22), isNull())).thenReturn( + classificationA); + when(mTextClassifier.classifyText(anyString(), eq(0), eq(22), isNull())).thenReturn( + classificationB); + + RemoteAction result = mClipboardUtils.getAction(textLinks.build(), "test").orElse(null); + + assertEquals(actionB, result); + } + + // TODO(b/267162944): Next four tests (marked "legacy") are obsolete once + // CLIPBOARD_MINIMIZED_LAYOUT flag is released and removed + @Test + public void test_getAction_noLinks_returnsEmptyOptional_legacy() { ClipData.Item item = new ClipData.Item("no text links"); item.setTextLinks(Mockito.mock(TextLinks.class)); @@ -86,8 +155,8 @@ public class ClipboardOverlayUtilsTest extends SysuiTestCase { } @Test - public void test_getAction_returnsFirstLink() { - when(mClipDataItem.getTextLinks()).thenReturn(getFakeTextLinks()); + public void test_getAction_returnsFirstLink_legacy() { + when(mClipDataItem.getTextLinks()).thenReturn(getFakeTextLinksBuilder().build()); when(mClipDataItem.getText()).thenReturn(""); RemoteAction actionA = constructRemoteAction("abc"); RemoteAction actionB = constructRemoteAction("def"); @@ -98,14 +167,14 @@ public class ClipboardOverlayUtilsTest extends SysuiTestCase { when(mTextClassifier.classifyText(anyString(), anyInt(), anyInt(), isNull())).thenReturn( classificationA, classificationB); - RemoteAction result = mClipboardUtils.getAction(mClipDataItem, "def").orElse(null); + RemoteAction result = mClipboardUtils.getAction(mClipDataItem, "test").orElse(null); assertEquals(actionA, result); } @Test - public void test_getAction_skipsMatchingComponent() { - when(mClipDataItem.getTextLinks()).thenReturn(getFakeTextLinks()); + public void test_getAction_skipsMatchingComponent_legacy() { + when(mClipDataItem.getTextLinks()).thenReturn(getFakeTextLinksBuilder().build()); when(mClipDataItem.getText()).thenReturn(""); RemoteAction actionA = constructRemoteAction("abc"); RemoteAction actionB = constructRemoteAction("def"); @@ -122,6 +191,33 @@ public class ClipboardOverlayUtilsTest extends SysuiTestCase { } @Test + public void test_getAction_skipsShortEntity_legacy() { + TextLinks.Builder textLinks = new TextLinks.Builder("test text of length 22"); + final Map<String, Float> scores = new ArrayMap<>(); + scores.put(TextClassifier.TYPE_EMAIL, 1f); + textLinks.addLink(20, 22, scores); + textLinks.addLink(0, 22, scores); + + when(mClipDataItem.getTextLinks()).thenReturn(textLinks.build()); + when(mClipDataItem.getText()).thenReturn(textLinks.build().getText()); + + RemoteAction actionA = constructRemoteAction("abc"); + RemoteAction actionB = constructRemoteAction("def"); + TextClassification classificationA = Mockito.mock(TextClassification.class); + when(classificationA.getActions()).thenReturn(Lists.newArrayList(actionA)); + TextClassification classificationB = Mockito.mock(TextClassification.class); + when(classificationB.getActions()).thenReturn(Lists.newArrayList(actionB)); + when(mTextClassifier.classifyText(anyString(), eq(20), eq(22), isNull())).thenReturn( + classificationA); + when(mTextClassifier.classifyText(anyString(), eq(0), eq(22), isNull())).thenReturn( + classificationB); + + RemoteAction result = mClipboardUtils.getAction(mClipDataItem, "test").orElse(null); + + assertEquals(actionB, result); + } + + @Test public void test_extra_withPackage_returnsTrue() { PersistableBundle b = new PersistableBundle(); b.putBoolean(ClipDescription.EXTRA_IS_REMOTE_DEVICE, true); @@ -184,12 +280,12 @@ public class ClipboardOverlayUtilsTest extends SysuiTestCase { return action; } - private static TextLinks getFakeTextLinks() { - TextLinks.Builder textLinks = new TextLinks.Builder("test"); + private static TextLinks.Builder getFakeTextLinksBuilder() { + TextLinks.Builder textLinks = new TextLinks.Builder("test text of length 22"); final Map<String, Float> scores = new ArrayMap<>(); scores.put(TextClassifier.TYPE_EMAIL, 1f); - textLinks.addLink(0, 0, scores); - textLinks.addLink(0, 0, scores); - return textLinks.build(); + textLinks.addLink(0, 22, scores); + textLinks.addLink(0, 22, scores); + return textLinks; } } 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/dreams/complication/ComplicationHostViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java index dcd8736711f6..068852de7a43 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java @@ -22,6 +22,8 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.os.UserHandle; +import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.view.View; @@ -33,6 +35,8 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.dreams.DreamOverlayStateController; +import com.android.systemui.util.settings.FakeSettings; +import com.android.systemui.util.settings.SecureSettings; import org.junit.Before; import org.junit.Test; @@ -96,6 +100,10 @@ public class ComplicationHostViewControllerTest extends SysuiTestCase { private ComplicationHostViewController mController; + private SecureSettings mSecureSettings; + + private static final int CURRENT_USER_ID = UserHandle.USER_SYSTEM; + @Before public void setup() { MockitoAnnotations.initMocks(this); @@ -108,12 +116,17 @@ public class ComplicationHostViewControllerTest extends SysuiTestCase { when(mViewHolder.getLayoutParams()).thenReturn(mComplicationLayoutParams); when(mComplicationView.getParent()).thenReturn(mComplicationHostView); + mSecureSettings = new FakeSettings(); + mSecureSettings.putFloatForUser( + Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f, CURRENT_USER_ID); + mController = new ComplicationHostViewController( mComplicationHostView, mLayoutEngine, mDreamOverlayStateController, mLifecycleOwner, - mViewModel); + mViewModel, + mSecureSettings); mController.init(); } @@ -188,6 +201,23 @@ public class ComplicationHostViewControllerTest extends SysuiTestCase { verify(mComplicationView, never()).setVisibility(View.INVISIBLE); } + @Test + public void testAnimationsDisabled_ComplicationsNeverSetToInvisible() { + //Disable animations + mController.mIsAnimationEnabled = false; + + final Observer<Collection<ComplicationViewModel>> observer = + captureComplicationViewModelsObserver(); + + // Add a complication before entry animations are finished. + final HashSet<ComplicationViewModel> complications = new HashSet<>( + Collections.singletonList(mComplicationViewModel)); + observer.onChanged(complications); + + // The complication view should not be set to invisible. + verify(mComplicationView, never()).setVisibility(View.INVISIBLE); + } + private Observer<Collection<ComplicationViewModel>> captureComplicationViewModelsObserver() { verify(mComplicationViewModelLiveData).observe(eq(mLifecycleOwner), mObserverCaptor.capture()); 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/keyboard/backlight/domain/interactor/KeyboardBacklightInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/domain/interactor/KeyboardBacklightInteractorTest.kt new file mode 100644 index 000000000000..ec94cdec78f0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/domain/interactor/KeyboardBacklightInteractorTest.kt @@ -0,0 +1,73 @@ +/* + * 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.keyboard.backlight.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository +import com.android.systemui.keyboard.shared.model.BacklightModel +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class KeyboardBacklightInteractorTest : SysuiTestCase() { + + private val keyboardRepository = FakeKeyboardRepository() + private lateinit var underTest: KeyboardBacklightInteractor + + @Before + fun setUp() { + underTest = KeyboardBacklightInteractor(keyboardRepository) + } + + @Test + fun emitsNull_whenKeyboardJustConnected() = runTest { + val latest by collectLastValue(underTest.backlight) + keyboardRepository.setKeyboardConnected(true) + + assertThat(latest).isNull() + } + + @Test + fun emitsBacklight_whenKeyboardConnectedAndBacklightChanged() = runTest { + keyboardRepository.setKeyboardConnected(true) + keyboardRepository.setBacklight(BacklightModel(1, 5)) + + assertThat(underTest.backlight.first()).isEqualTo(BacklightModel(1, 5)) + } + + @Test + fun emitsNull_afterKeyboardDisconnecting() = runTest { + val latest by collectLastValue(underTest.backlight) + keyboardRepository.setKeyboardConnected(true) + keyboardRepository.setBacklight(BacklightModel(1, 5)) + + keyboardRepository.setKeyboardConnected(false) + + assertThat(latest).isNull() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogViewModelTest.kt new file mode 100644 index 000000000000..ec05d10b793c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogViewModelTest.kt @@ -0,0 +1,102 @@ +/* + * 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.keyboard.backlight.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyboard.backlight.domain.interactor.KeyboardBacklightInteractor +import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository +import com.android.systemui.keyboard.shared.model.BacklightModel +import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class BacklightDialogViewModelTest : SysuiTestCase() { + + private val keyboardRepository = FakeKeyboardRepository() + private lateinit var underTest: BacklightDialogViewModel + @Mock private lateinit var accessibilityManagerWrapper: AccessibilityManagerWrapper + private val timeoutMillis = 3000L + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(accessibilityManagerWrapper.getRecommendedTimeoutMillis(any(), any())) + .thenReturn(timeoutMillis.toInt()) + underTest = + BacklightDialogViewModel( + KeyboardBacklightInteractor(keyboardRepository), + accessibilityManagerWrapper + ) + keyboardRepository.setKeyboardConnected(true) + } + + @Test + fun emitsViewModel_whenBacklightChanged() = runTest { + keyboardRepository.setBacklight(BacklightModel(1, 5)) + + assertThat(underTest.dialogContent.first()).isEqualTo(BacklightDialogContentViewModel(1, 5)) + } + + @Test + fun emitsNull_afterTimeout() = runTest { + val latest by collectLastValue(underTest.dialogContent) + keyboardRepository.setBacklight(BacklightModel(1, 5)) + + assertThat(latest).isEqualTo(BacklightDialogContentViewModel(1, 5)) + advanceTimeBy(timeoutMillis + 1) + assertThat(latest).isNull() + } + + @Test + fun emitsNull_after5secDelay_fromLastBacklightChange() = runTest { + val latest by collectLastValue(underTest.dialogContent) + keyboardRepository.setKeyboardConnected(true) + + keyboardRepository.setBacklight(BacklightModel(1, 5)) + assertThat(latest).isEqualTo(BacklightDialogContentViewModel(1, 5)) + + advanceTimeBy(timeoutMillis * 2 / 3) + // timeout yet to pass, no new emission + keyboardRepository.setBacklight(BacklightModel(2, 5)) + assertThat(latest).isEqualTo(BacklightDialogContentViewModel(2, 5)) + + advanceTimeBy(timeoutMillis * 2 / 3) + // timeout refreshed because of last `setBacklight`, still content present + assertThat(latest).isEqualTo(BacklightDialogContentViewModel(2, 5)) + + advanceTimeBy(timeoutMillis * 2 / 3) + // finally timeout reached and null emitted + assertThat(latest).isNull() + } +} 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 new file mode 100644 index 000000000000..746f66881a88 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt @@ -0,0 +1,180 @@ +/* + * 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.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +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 +import com.android.systemui.util.mockito.whenever +import com.google.common.collect.Range +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidJUnit4::class) +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, + primaryBouncerInteractor + ) + + whenever(primaryBouncerInteractor.willRunDismissFromKeyguard()).thenReturn(false) + whenever(statusBarStateController.leaveOpenOnKeyguardHide()).thenReturn(false) + } + + @Test + fun bouncerAlpha() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf<Float>() + + 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) + + 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, behindAlpha = 1f)) + } + + job.cancel() + } + + @Test + fun scrimBehindAlpha_doNotLeaveShadeOpen() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf<ScrimAlpha>() + + val job = underTest.scrimAlpha.onEach { values.add(it) }.launchIn(this) + + whenever(statusBarStateController.leaveOpenOnKeyguardHide()).thenReturn(false) + + 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.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() + } + + private fun step( + value: Float, + state: TransitionState = TransitionState.RUNNING + ): TransitionStep { + return TransitionStep( + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.GONE, + value = value, + transitionState = state, + ownerName = "PrimaryBouncerToGoneTransitionViewModelTest" + ) + } +} 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/media/controls/ui/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt index 7f5707722b9c..e0ca90ec2c58 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt @@ -18,7 +18,6 @@ package com.android.systemui.media.controls.ui import android.app.PendingIntent import android.content.res.ColorStateList -import android.content.res.Configuration import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.util.MathUtils.abs @@ -685,46 +684,6 @@ class MediaCarouselControllerTest : SysuiTestCase() { } @Test - fun testOnConfigChanged_playersAreAddedBack() { - mediaCarouselController.pageIndicator = pageIndicator - - listener.value.onMediaDataLoaded( - "playing local", - null, - DATA.copy( - active = true, - isPlaying = true, - playbackLocation = MediaData.PLAYBACK_LOCAL, - resumption = false - ) - ) - listener.value.onMediaDataLoaded( - "paused local", - null, - DATA.copy( - active = true, - isPlaying = false, - playbackLocation = MediaData.PLAYBACK_LOCAL, - resumption = false - ) - ) - runAllReady() - - val playersSize = MediaPlayerData.players().size - - configListener.value.onConfigChanged(Configuration()) - runAllReady() - - verify(pageIndicator).tintList = - ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator)) - assertEquals(playersSize, MediaPlayerData.players().size) - assertEquals( - MediaPlayerData.getMediaPlayerIndex("playing local"), - mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex - ) - } - - @Test fun testOnUiModeChanged_playersAreAddedBack() { mediaCarouselController.pageIndicator = pageIndicator diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt index 55a33b6636e0..fd353afff7c0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt @@ -27,6 +27,7 @@ import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color +import android.graphics.Matrix import android.graphics.drawable.Animatable2 import android.graphics.drawable.AnimatedVectorDrawable import android.graphics.drawable.Drawable @@ -78,6 +79,8 @@ import com.android.systemui.media.controls.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA import com.android.systemui.media.controls.pipeline.MediaDataManager import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.media.dialog.MediaOutputDialogFactory +import com.android.systemui.monet.ColorScheme +import com.android.systemui.monet.Style import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.NotificationLockscreenUserManager @@ -214,6 +217,7 @@ public class MediaControlPanelTest : SysuiTestCase() { @Mock private lateinit var recSubtitleMock2: TextView @Mock private lateinit var recSubtitleMock3: TextView @Mock private lateinit var coverItem: ImageView + @Mock private lateinit var matrix: Matrix private lateinit var coverItem1: ImageView private lateinit var coverItem2: ImageView private lateinit var coverItem3: ImageView @@ -700,6 +704,46 @@ public class MediaControlPanelTest : SysuiTestCase() { } @Test + fun addTwoPlayerGradients_differentStates() { + // Setup redArtwork and its color scheme. + val redBmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888) + val redCanvas = Canvas(redBmp) + redCanvas.drawColor(Color.RED) + val redArt = Icon.createWithBitmap(redBmp) + val redWallpaperColor = player.getWallpaperColor(redArt) + val redColorScheme = ColorScheme(redWallpaperColor, true, Style.CONTENT) + + // Setup greenArt and its color scheme. + val greenBmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888) + val greenCanvas = Canvas(greenBmp) + greenCanvas.drawColor(Color.GREEN) + val greenArt = Icon.createWithBitmap(greenBmp) + val greenWallpaperColor = player.getWallpaperColor(greenArt) + val greenColorScheme = ColorScheme(greenWallpaperColor, true, Style.CONTENT) + + // Add gradient to both icons. + val redArtwork = player.addGradientToPlayerAlbum(redArt, redColorScheme, 10, 10) + val greenArtwork = player.addGradientToPlayerAlbum(greenArt, greenColorScheme, 10, 10) + + // They should have different constant states as they have different gradient color. + assertThat(redArtwork.getDrawable(1).constantState) + .isNotEqualTo(greenArtwork.getDrawable(1).constantState) + } + + @Test + fun getWallpaperColor_recycledBitmap_notCrashing() { + // Setup redArt icon. + val redBmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888) + val redArt = Icon.createWithBitmap(redBmp) + + // Recycle bitmap of redArt icon. + redArt.bitmap.recycle() + + // get wallpaperColor without illegal exception. + player.getWallpaperColor(redArt) + } + + @Test fun bind_seekBarDisabled_hasActions_seekBarVisibilityIsSetToInvisible() { useRealConstraintSets() @@ -2092,6 +2136,7 @@ public class MediaControlPanelTest : SysuiTestCase() { .thenReturn(listOf(recProgressBar1, recProgressBar2, recProgressBar3)) whenever(recommendationViewHolder.mediaSubtitles) .thenReturn(listOf(recSubtitleMock1, recSubtitleMock2, recSubtitleMock3)) + whenever(coverItem.imageMatrix).thenReturn(matrix) val bmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888) val canvas = Canvas(bmp) @@ -2127,6 +2172,7 @@ public class MediaControlPanelTest : SysuiTestCase() { verify(recCardTitle).setTextColor(any<Int>()) verify(recAppIconItem, times(3)).setImageDrawable(any(Drawable::class.java)) verify(coverItem, times(3)).setImageDrawable(any(Drawable::class.java)) + verify(coverItem, times(3)).imageMatrix = any() } @Test @@ -2189,6 +2235,34 @@ public class MediaControlPanelTest : SysuiTestCase() { } @Test + fun addTwoRecommendationGradients_differentStates() { + // Setup redArtwork and its color scheme. + val redBmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888) + val redCanvas = Canvas(redBmp) + redCanvas.drawColor(Color.RED) + val redArt = Icon.createWithBitmap(redBmp) + val redWallpaperColor = player.getWallpaperColor(redArt) + val redColorScheme = ColorScheme(redWallpaperColor, true, Style.CONTENT) + + // Setup greenArt and its color scheme. + val greenBmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888) + val greenCanvas = Canvas(greenBmp) + greenCanvas.drawColor(Color.GREEN) + val greenArt = Icon.createWithBitmap(greenBmp) + val greenWallpaperColor = player.getWallpaperColor(greenArt) + val greenColorScheme = ColorScheme(greenWallpaperColor, true, Style.CONTENT) + + // Add gradient to both icons. + val redArtwork = player.addGradientToRecommendationAlbum(redArt, redColorScheme, 10, 10) + val greenArtwork = + player.addGradientToRecommendationAlbum(greenArt, greenColorScheme, 10, 10) + + // They should have different constant states as they have different gradient color. + assertThat(redArtwork.getDrawable(1).constantState) + .isNotEqualTo(greenArtwork.getDrawable(1).constantState) + } + + @Test fun onButtonClick_touchRippleFlagEnabled_playsTouchRipple() { fakeFeatureFlag.set(Flags.UMO_SURFACE_RIPPLE, true) val semanticActions = diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt index af91cdb1522c..0fac3db2dc1f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt @@ -16,9 +16,12 @@ package com.android.systemui.media.controls.ui +import android.content.res.Configuration +import android.content.res.Configuration.ORIENTATION_LANDSCAPE import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View +import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase @@ -58,6 +61,8 @@ class MediaViewControllerTest : SysuiTestCase() { @Mock private lateinit var mediaSubTitleWidgetState: WidgetState @Mock private lateinit var mediaContainerWidgetState: WidgetState @Mock private lateinit var mediaFlags: MediaFlags + @Mock private lateinit var expandedLayout: ConstraintSet + @Mock private lateinit var collapsedLayout: ConstraintSet val delta = 0.1F @@ -77,6 +82,19 @@ class MediaViewControllerTest : SysuiTestCase() { } @Test + fun testOrientationChanged_layoutsAreLoaded() { + mediaViewController.expandedLayout = expandedLayout + mediaViewController.collapsedLayout = collapsedLayout + + val newConfig = Configuration() + newConfig.orientation = ORIENTATION_LANDSCAPE + configurationController.onConfigurationChanged(newConfig) + + verify(expandedLayout).load(context, R.xml.media_session_expanded) + verify(collapsedLayout).load(context, R.xml.media_session_collapsed) + } + + @Test fun testObtainViewState_applySquishFraction_toPlayerTransitionViewState_height() { mediaViewController.attach(player, MediaViewController.TYPE.PLAYER) player.measureState = 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/qs/external/CustomTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt index 8644b5ea18ae..5be95d6f6255 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt @@ -57,6 +57,7 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito.`when` +import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.reset @@ -351,4 +352,44 @@ class CustomTileTest : SysuiTestCase() { .startPendingIntentDismissingKeyguard( eq(pi), nullable(), nullable<ActivityLaunchAnimator.Controller>()) } + + @Test + fun testActiveTileListensOnceAfterCreated() { + `when`(tileServiceManager.isActiveTile).thenReturn(true) + + val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext) + tile.initialize() + tile.postStale() + testableLooper.processAllMessages() + + verify(tileServiceManager).setBindRequested(true) + verify(tileService).onStartListening() + } + + @Test + fun testActiveTileDoesntListenAfterFirstTime() { + `when`(tileServiceManager.isActiveTile).thenReturn(true) + + val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext) + tile.initialize() + // Make sure we have an icon in the tile because we don't have a default icon + // This should not be overridden by the retrieved tile that has null icon. + tile.qsTile.icon = mock(Icon::class.java) + `when`(tile.qsTile.icon.loadDrawable(any(Context::class.java))) + .thenReturn(mock(Drawable::class.java)) + + tile.postStale() + testableLooper.processAllMessages() + + // postStale will set it to not listening after it's done + verify(tileService).onStopListening() + + clearInvocations(tileServiceManager, tileService) + + tile.setListening(Any(), true) + testableLooper.processAllMessages() + + verify(tileServiceManager, never()).setBindRequested(true) + verify(tileService, never()).onStartListening() + } }
\ No newline at end of file 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 52b0b6abda1d..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; @@ -551,6 +551,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mNotificationStackSizeCalculator, mUnlockedScreenOffAnimationController, mShadeTransitionController, + mInteractionJankMonitor, systemClock, mKeyguardBottomAreaViewModel, mKeyguardBottomAreaInteractor, 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 dd7143ae7e16..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 @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.stack; import static android.view.View.GONE; +import static android.view.WindowInsets.Type.ime; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_GENTLE; @@ -46,6 +47,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.graphics.Insets; import android.graphics.Rect; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -54,6 +56,8 @@ import android.util.MathUtils; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.WindowInsets; +import android.view.WindowInsetsAnimation; import android.widget.TextView; import androidx.test.annotation.UiThreadTest; @@ -64,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; @@ -91,6 +97,8 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.util.ArrayList; + /** * Tests for {@link NotificationStackScrollLayout}. */ @@ -123,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 @@ -136,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); @@ -843,6 +856,19 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { verify(mEmptyShadeView).setFooterText(not(0)); } + @Test + public void testWindowInsetAnimationProgress_updatesBottomInset() { + int bottomImeInset = 100; + mStackScrollerInternal.setAnimatedInsetsEnabled(true); + WindowInsets windowInsets = new WindowInsets.Builder() + .setInsets(ime(), Insets.of(0, 0, 0, bottomImeInset)).build(); + ArrayList<WindowInsetsAnimation> windowInsetsAnimations = new ArrayList<>(); + mStackScrollerInternal + .dispatchWindowInsetsAnimationProgress(windowInsets, windowInsetsAnimations); + + assertEquals(bottomImeInset, mStackScrollerInternal.mBottomInset); + } + private void setBarStateForTest(int state) { // Can't inject this through the listener or we end up on the actual implementation // rather than the mock because the spy just coppied the anonymous inner /shruggie. diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/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/DozeParametersTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java index eb5edbc21d89..f5b7ca804fbc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java @@ -180,6 +180,7 @@ public class DozeParametersTest extends SysuiTestCase { when(mAmbientDisplayConfiguration.alwaysOnEnabled(anyInt())).thenReturn(true); mDozeParameters.onTuningChanged(Settings.Secure.DOZE_ALWAYS_ON, "1"); + verify(mScreenOffAnimationController).onAlwaysOnChanged(false); assertThat(mDozeParameters.getAlwaysOn()).isFalse(); } @@ -196,13 +197,16 @@ public class DozeParametersTest extends SysuiTestCase { mBatteryStateChangeCallback.getValue().onPowerSaveChanged(true); verify(callback, times(2)).onAlwaysOnChange(); + verify(mScreenOffAnimationController, times(2)).onAlwaysOnChanged(false); assertThat(mDozeParameters.getAlwaysOn()).isFalse(); + reset(mScreenOffAnimationController); reset(callback); when(mBatteryController.isAodPowerSave()).thenReturn(false); mBatteryStateChangeCallback.getValue().onPowerSaveChanged(true); verify(callback).onAlwaysOnChange(); + verify(mScreenOffAnimationController).onAlwaysOnChanged(true); assertThat(mDozeParameters.getAlwaysOn()).isTrue(); } 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 180d9f8956c3..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,6 +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.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; @@ -103,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; @@ -131,6 +137,7 @@ public class ScrimControllerTest extends SysuiTestCase { // 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; @@ -240,18 +247,28 @@ public class ScrimControllerTest extends SysuiTestCase { when(mKeyguardTransitionInteractor.getPrimaryBouncerToGoneTransition()) .thenReturn(emptyFlow()); - when(mPrimaryBouncerToGoneTransitionViewModel.getScrimAlpha()).thenReturn(emptyFlow()); + 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, - mMainDispatcher); + mMainDispatcher, + mLinearLargeScreenShadeInterpolator, + mFeatureFlags); mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible); mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront); mScrimController.setAnimatorListener(mAnimatorListener); @@ -649,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); @@ -687,7 +778,6 @@ public class ScrimControllerTest extends SysuiTestCase { mScrimBehind, OPAQUE)); } - @Test public void scrimStateCallback() { mScrimController.transitionTo(ScrimState.UNLOCKED); @@ -875,16 +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, - mMainDispatcher); + mMainDispatcher, + mLinearLargeScreenShadeInterpolator, + mFeatureFlags); mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible); mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront); mScrimController.setAnimatorListener(mAnimatorListener); @@ -1664,6 +1763,16 @@ public class ScrimControllerTest extends SysuiTestCase { assertThat(mScrimController.getState()).isEqualTo(ScrimState.UNLOCKED); } + @Test + public void primaryBouncerToGoneOnFinishCallsKeyguardFadedAway() { + when(mKeyguardStateController.isKeyguardFadingAway()).thenReturn(true); + mScrimController.mPrimaryBouncerToGoneTransition.accept( + new TransitionStep(KeyguardState.PRIMARY_BOUNCER, KeyguardState.GONE, 0f, + TransitionState.FINISHED, "ScrimControllerTest")); + + verify(mStatusBarKeyguardViewManager).onKeyguardFadedAway(); + } + private void assertAlphaAfterExpansion(ScrimView scrim, float expectedAlpha, float expansion) { mScrimController.setRawPanelExpansionFraction(expansion); finishAnimationsImmediately(); 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/StatusBarMoveFromCenterAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationControllerTest.kt index 1779de729e5b..7594c90daa8b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationControllerTest.kt @@ -8,13 +8,14 @@ import android.view.WindowManager import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.unfold.TestUnfoldTransitionProvider +import com.android.systemui.unfold.util.CurrentActivityTypeProvider import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.mockito.ArgumentMatchers.any import org.mockito.Mock -import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @SmallTest @@ -26,6 +27,9 @@ class StatusBarMoveFromCenterAnimationControllerTest : SysuiTestCase() { @Mock private lateinit var display: Display + @Mock + private lateinit var currentActivityTypeProvider: CurrentActivityTypeProvider + private val view: View = View(context) private val progressProvider = TestUnfoldTransitionProvider() private val scopedProvider = ScopedUnfoldTransitionProgressProvider(progressProvider) @@ -36,9 +40,9 @@ class StatusBarMoveFromCenterAnimationControllerTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - `when`(windowManager.defaultDisplay).thenReturn(display) - `when`(display.rotation).thenReturn(Surface.ROTATION_0) - `when`(display.getSize(any())).thenAnswer { + whenever(windowManager.defaultDisplay).thenReturn(display) + whenever(display.rotation).thenReturn(Surface.ROTATION_0) + whenever(display.getSize(any())).thenAnswer { val point = it.arguments[0] as Point point.x = 100 point.y = 100 @@ -47,7 +51,12 @@ class StatusBarMoveFromCenterAnimationControllerTest : SysuiTestCase() { scopedProvider.setReadyToHandleTransition(true) - controller = StatusBarMoveFromCenterAnimationController(scopedProvider, windowManager) + controller = + StatusBarMoveFromCenterAnimationController( + scopedProvider, + currentActivityTypeProvider, + windowManager + ) } @Test @@ -99,6 +108,31 @@ class StatusBarMoveFromCenterAnimationControllerTest : SysuiTestCase() { } @Test + fun alpha_onLauncher_alphaDoesNotChange() { + whenever(currentActivityTypeProvider.isHomeActivity).thenReturn(true) + controller.onViewsReady(arrayOf(view)) + progressProvider.onTransitionStarted() + progressProvider.onTransitionProgress(0.0f) + assertThat(view.alpha).isEqualTo(1.0f) + + progressProvider.onTransitionProgress(1.0f) + + assertThat(view.alpha).isEqualTo(1.0f) + } + + @Test + fun alpha_NotOnLauncher_alphaChanges() { + whenever(currentActivityTypeProvider.isHomeActivity).thenReturn(false) + controller.onViewsReady(arrayOf(view)) + progressProvider.onTransitionStarted() + assertThat(view.alpha).isEqualTo(1.0f) + + progressProvider.onTransitionProgress(0.5f) + + assertThat(view.alpha).isNotEqualTo(1.0f) + } + + @Test fun transitionFinished_viewReAttached_noChangesToTranslation() { controller.onViewsReady(arrayOf(view)) progressProvider.onTransitionProgress(0.5f) 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/shared/MobileInputLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLoggerTest.kt index 86529dce948a..7c9351c8495d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/shared/MobileInputLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLoggerTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.statusbar.pipeline.mobile.shared +package com.android.systemui.statusbar.pipeline.mobile.data import android.net.Network import android.net.NetworkCapabilities @@ -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/CarrierConfigRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepositoryTest.kt index 0145103d55e1..dfef62e95eda 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepositoryTest.kt @@ -24,8 +24,8 @@ import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager +import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfigTest.Companion.createTestConfig -import com.android.systemui.statusbar.pipeline.mobile.shared.MobileInputLogger import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi 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/MobileRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt index 17502f28a479..07c8cee9a3d4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt @@ -27,13 +27,13 @@ import com.android.systemui.demomode.DemoModeController import com.android.systemui.dump.DumpManager import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.TableLogBufferFactory +import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoModeMobileConnectionDataSource import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.validMobileEvent import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryImpl -import com.android.systemui.statusbar.pipeline.mobile.shared.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource 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 b2577e349da7..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,12 +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 @@ -65,7 +68,6 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrier import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS -import com.android.systemui.statusbar.pipeline.mobile.shared.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel @@ -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/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt index 09b7a66c925d..68b1cda62f4c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt @@ -37,12 +37,12 @@ import com.android.settingslib.mobile.MobileMappings import com.android.systemui.SysuiTestCase import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.TableLogBufferFactory +import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.CarrierConfigRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Factory.Companion.tableBufferLogName -import com.android.systemui.statusbar.pipeline.mobile.shared.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt index fa072fc366eb..1eb1056204cd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -23,7 +23,6 @@ import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.CarrierMergedNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType @@ -74,9 +73,7 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun gsm_level_default_unknown() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel(isGsm = true), - ) + connectionRepository.isGsm.value = true var latest: Int? = null val job = underTest.level.onEach { latest = it }.launchIn(this) @@ -89,13 +86,9 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun gsm_usesGsmLevel() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = true, - primaryLevel = GSM_LEVEL, - cdmaLevel = CDMA_LEVEL - ), - ) + connectionRepository.isGsm.value = true + connectionRepository.primaryLevel.value = GSM_LEVEL + connectionRepository.cdmaLevel.value = CDMA_LEVEL var latest: Int? = null val job = underTest.level.onEach { latest = it }.launchIn(this) @@ -108,13 +101,9 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun gsm_alwaysShowCdmaTrue_stillUsesGsmLevel() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = true, - primaryLevel = GSM_LEVEL, - cdmaLevel = CDMA_LEVEL, - ), - ) + connectionRepository.isGsm.value = true + connectionRepository.primaryLevel.value = GSM_LEVEL + connectionRepository.cdmaLevel.value = CDMA_LEVEL mobileIconsInteractor.alwaysUseCdmaLevel.value = true var latest: Int? = null @@ -128,9 +117,7 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun notGsm_level_default_unknown() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel(isGsm = false), - ) + connectionRepository.isGsm.value = false var latest: Int? = null val job = underTest.level.onEach { latest = it }.launchIn(this) @@ -142,13 +129,9 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun notGsm_alwaysShowCdmaTrue_usesCdmaLevel() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = false, - primaryLevel = GSM_LEVEL, - cdmaLevel = CDMA_LEVEL - ), - ) + connectionRepository.isGsm.value = false + connectionRepository.primaryLevel.value = GSM_LEVEL + connectionRepository.cdmaLevel.value = CDMA_LEVEL mobileIconsInteractor.alwaysUseCdmaLevel.value = true var latest: Int? = null @@ -162,13 +145,9 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun notGsm_alwaysShowCdmaFalse_usesPrimaryLevel() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = false, - primaryLevel = GSM_LEVEL, - cdmaLevel = CDMA_LEVEL, - ), - ) + connectionRepository.isGsm.value = false + connectionRepository.primaryLevel.value = GSM_LEVEL + connectionRepository.cdmaLevel.value = CDMA_LEVEL mobileIconsInteractor.alwaysUseCdmaLevel.value = false var latest: Int? = null @@ -197,11 +176,8 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun iconGroup_three_g() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - resolvedNetworkType = DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) - ), - ) + connectionRepository.resolvedNetworkType.value = + DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) var latest: MobileIconGroup? = null val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) @@ -214,23 +190,14 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun iconGroup_updates_on_change() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - resolvedNetworkType = DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) - ), - ) + connectionRepository.resolvedNetworkType.value = + DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) var latest: MobileIconGroup? = null val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) - connectionRepository.setConnectionInfo( - MobileConnectionModel( - resolvedNetworkType = - DefaultNetworkType( - mobileMappingsProxy.toIconKey(FOUR_G), - ), - ), - ) + connectionRepository.resolvedNetworkType.value = + DefaultNetworkType(mobileMappingsProxy.toIconKey(FOUR_G)) yield() assertThat(latest).isEqualTo(TelephonyIcons.FOUR_G) @@ -241,12 +208,8 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun iconGroup_5g_override_type() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - resolvedNetworkType = - OverrideNetworkType(mobileMappingsProxy.toIconKeyOverride(FIVE_G_OVERRIDE)) - ), - ) + connectionRepository.resolvedNetworkType.value = + OverrideNetworkType(mobileMappingsProxy.toIconKeyOverride(FIVE_G_OVERRIDE)) var latest: MobileIconGroup? = null val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) @@ -259,12 +222,8 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun iconGroup_default_if_no_lookup() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - resolvedNetworkType = - DefaultNetworkType(mobileMappingsProxy.toIconKey(NETWORK_TYPE_UNKNOWN)), - ), - ) + connectionRepository.resolvedNetworkType.value = + DefaultNetworkType(mobileMappingsProxy.toIconKey(NETWORK_TYPE_UNKNOWN)) var latest: MobileIconGroup? = null val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) @@ -277,11 +236,7 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun iconGroup_carrierMerged_usesOverride() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - resolvedNetworkType = CarrierMergedNetworkType, - ), - ) + connectionRepository.resolvedNetworkType.value = CarrierMergedNetworkType var latest: MobileIconGroup? = null val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) @@ -295,11 +250,8 @@ class MobileIconInteractorTest : SysuiTestCase() { fun `icon group - checks default data`() = runBlocking(IMMEDIATE) { mobileIconsInteractor.defaultDataSubId.value = SUB_1_ID - connectionRepository.setConnectionInfo( - MobileConnectionModel( - resolvedNetworkType = DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) - ), - ) + connectionRepository.resolvedNetworkType.value = + DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) var latest: MobileIconGroup? = null val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) @@ -380,9 +332,7 @@ class MobileIconInteractorTest : SysuiTestCase() { var latest: Boolean? = null val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this) - connectionRepository.setConnectionInfo( - MobileConnectionModel(dataConnectionState = DataConnectionState.Connected) - ) + connectionRepository.dataConnectionState.value = DataConnectionState.Connected yield() assertThat(latest).isTrue() @@ -396,9 +346,7 @@ class MobileIconInteractorTest : SysuiTestCase() { var latest: Boolean? = null val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this) - connectionRepository.setConnectionInfo( - MobileConnectionModel(dataConnectionState = DataConnectionState.Disconnected) - ) + connectionRepository.dataConnectionState.value = DataConnectionState.Disconnected assertThat(latest).isFalse() @@ -411,11 +359,11 @@ class MobileIconInteractorTest : SysuiTestCase() { var latest: Boolean? = null val job = underTest.isInService.onEach { latest = it }.launchIn(this) - connectionRepository.setConnectionInfo(MobileConnectionModel(isInService = true)) + connectionRepository.isInService.value = true assertThat(latest).isTrue() - connectionRepository.setConnectionInfo(MobileConnectionModel(isInService = false)) + connectionRepository.isInService.value = false assertThat(latest).isFalse() @@ -429,22 +377,13 @@ class MobileIconInteractorTest : SysuiTestCase() { val job = underTest.isRoaming.onEach { latest = it }.launchIn(this) connectionRepository.cdmaRoaming.value = true - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = true, - isRoaming = false, - ) - ) + connectionRepository.isGsm.value = true + connectionRepository.isRoaming.value = false yield() assertThat(latest).isFalse() - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = true, - isRoaming = true, - ) - ) + connectionRepository.isRoaming.value = true yield() assertThat(latest).isTrue() @@ -459,23 +398,15 @@ class MobileIconInteractorTest : SysuiTestCase() { val job = underTest.isRoaming.onEach { latest = it }.launchIn(this) connectionRepository.cdmaRoaming.value = false - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = false, - isRoaming = true, - ) - ) + connectionRepository.isGsm.value = false + connectionRepository.isRoaming.value = true yield() assertThat(latest).isFalse() connectionRepository.cdmaRoaming.value = true - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = false, - isRoaming = false, - ) - ) + connectionRepository.isGsm.value = false + connectionRepository.isRoaming.value = false yield() assertThat(latest).isTrue() @@ -490,25 +421,15 @@ class MobileIconInteractorTest : SysuiTestCase() { val job = underTest.isRoaming.onEach { latest = it }.launchIn(this) connectionRepository.cdmaRoaming.value = true - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = false, - isRoaming = true, - carrierNetworkChangeActive = true, - ) - ) + connectionRepository.isGsm.value = false + connectionRepository.isRoaming.value = true + connectionRepository.carrierNetworkChangeActive.value = true yield() assertThat(latest).isFalse() connectionRepository.cdmaRoaming.value = true - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = true, - isRoaming = true, - carrierNetworkChangeActive = true, - ) - ) + connectionRepository.isGsm.value = true yield() assertThat(latest).isFalse() @@ -526,24 +447,20 @@ class MobileIconInteractorTest : SysuiTestCase() { // Default network name, operator name is non-null, uses the operator name connectionRepository.networkName.value = DEFAULT_NAME - connectionRepository.setConnectionInfo( - MobileConnectionModel(operatorAlphaShort = testOperatorName) - ) + connectionRepository.operatorAlphaShort.value = testOperatorName yield() assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived(testOperatorName)) // Default network name, operator name is null, uses the default - connectionRepository.setConnectionInfo(MobileConnectionModel(operatorAlphaShort = null)) + connectionRepository.operatorAlphaShort.value = null yield() assertThat(latest).isEqualTo(DEFAULT_NAME) // Derived network name, operator name non-null, uses the derived name connectionRepository.networkName.value = DERIVED_NAME - connectionRepository.setConnectionInfo( - MobileConnectionModel(operatorAlphaShort = testOperatorName) - ) + connectionRepository.operatorAlphaShort.value = testOperatorName yield() assertThat(latest).isEqualTo(DERIVED_NAME) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLoggerTest.kt new file mode 100644 index 000000000000..4aa48d6f25f1 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLoggerTest.kt @@ -0,0 +1,100 @@ +/* + * 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.ui + +import android.widget.TextView +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.DumpManager +import com.android.systemui.log.LogBufferFactory +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger.Companion.getIdForLogging +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.KeyguardMobileIconViewModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.QsMobileIconViewModel +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import java.io.PrintWriter +import java.io.StringWriter +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +class MobileViewLoggerTest : SysuiTestCase() { + private val buffer = LogBufferFactory(DumpManager(), mock()).create("buffer", 10) + private val stringWriter = StringWriter() + private val printWriter = PrintWriter(stringWriter) + + private val underTest = MobileViewLogger(buffer, mock()) + + @Mock private lateinit var flags: StatusBarPipelineFlags + @Mock private lateinit var commonViewModel: MobileIconViewModel + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun collectionStarted_dumpHasInfo() { + val view = TextView(context) + val viewModel = QsMobileIconViewModel(commonViewModel, flags) + + underTest.logCollectionStarted(view, viewModel) + + val dumpString = getDumpString() + assertThat(dumpString).contains("${view.getIdForLogging()}, isCollecting=true") + } + + @Test + fun collectionStarted_multipleViews_dumpHasInfo() { + val view = TextView(context) + val view2 = TextView(context) + val viewModel = QsMobileIconViewModel(commonViewModel, flags) + val viewModel2 = KeyguardMobileIconViewModel(commonViewModel, flags) + + underTest.logCollectionStarted(view, viewModel) + underTest.logCollectionStarted(view2, viewModel2) + + val dumpString = getDumpString() + assertThat(dumpString).contains("${view.getIdForLogging()}, isCollecting=true") + assertThat(dumpString).contains("${view2.getIdForLogging()}, isCollecting=true") + } + + @Test + fun collectionStopped_dumpHasInfo() { + val view = TextView(context) + val view2 = TextView(context) + val viewModel = QsMobileIconViewModel(commonViewModel, flags) + val viewModel2 = KeyguardMobileIconViewModel(commonViewModel, flags) + + underTest.logCollectionStarted(view, viewModel) + underTest.logCollectionStarted(view2, viewModel2) + underTest.logCollectionStopped(view, viewModel) + + val dumpString = getDumpString() + assertThat(dumpString).contains("${view.getIdForLogging()}, isCollecting=false") + assertThat(dumpString).contains("${view2.getIdForLogging()}, isCollecting=true") + } + + private fun getDumpString(): String { + underTest.dump(printWriter, args = arrayOf()) + return stringWriter.toString() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt index e68a3970ae93..7420db2e895e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt @@ -32,6 +32,7 @@ import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.QsMobileIconViewModel @@ -60,6 +61,7 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() { @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags @Mock private lateinit var tableLogBuffer: TableLogBuffer + @Mock private lateinit var viewLogger: MobileViewLogger @Mock private lateinit var constants: ConnectivityConstants private lateinit var interactor: FakeMobileIconInteractor private lateinit var airplaneModeRepository: FakeAirplaneModeRepository @@ -94,7 +96,13 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() { @Test fun setVisibleState_icon_iconShownDotHidden() { - val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel) + val view = + ModernStatusBarMobileView.constructAndBind( + context, + viewLogger, + SLOT_NAME, + viewModel, + ) view.setVisibleState(StatusBarIconView.STATE_ICON, /* animate= */ false) @@ -109,8 +117,13 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() { @Test fun setVisibleState_dot_iconHiddenDotShown() { - val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel) - + val view = + ModernStatusBarMobileView.constructAndBind( + context, + viewLogger, + SLOT_NAME, + viewModel, + ) view.setVisibleState(StatusBarIconView.STATE_DOT, /* animate= */ false) ViewUtils.attachView(view) @@ -124,8 +137,13 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() { @Test fun setVisibleState_hidden_iconAndDotHidden() { - val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel) - + val view = + ModernStatusBarMobileView.constructAndBind( + context, + viewLogger, + SLOT_NAME, + viewModel, + ) view.setVisibleState(StatusBarIconView.STATE_HIDDEN, /* animate= */ false) ViewUtils.attachView(view) @@ -142,8 +160,13 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() { whenever(constants.hasDataCapabilities).thenReturn(false) createViewModel() - val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel) - + val view = + ModernStatusBarMobileView.constructAndBind( + context, + viewLogger, + SLOT_NAME, + viewModel, + ) ViewUtils.attachView(view) testableLooper.processAllMessages() @@ -157,8 +180,13 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() { whenever(constants.hasDataCapabilities).thenReturn(true) createViewModel() - val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel) - + val view = + ModernStatusBarMobileView.constructAndBind( + context, + viewLogger, + SLOT_NAME, + viewModel, + ) ViewUtils.attachView(view) testableLooper.processAllMessages() @@ -171,8 +199,13 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() { fun isIconVisible_notAirplaneMode_outputsTrue() { airplaneModeRepository.setIsAirplaneMode(false) - val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel) - + val view = + ModernStatusBarMobileView.constructAndBind( + context, + viewLogger, + SLOT_NAME, + viewModel, + ) ViewUtils.attachView(view) testableLooper.processAllMessages() @@ -185,8 +218,13 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() { fun isIconVisible_airplaneMode_outputsTrue() { airplaneModeRepository.setIsAirplaneMode(true) - val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel) - + val view = + ModernStatusBarMobileView.constructAndBind( + context, + viewLogger, + SLOT_NAME, + viewModel, + ) ViewUtils.attachView(view) testableLooper.processAllMessages() @@ -198,7 +236,13 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() { @Test fun onDarkChanged_iconHasNewColor() { whenever(statusBarPipelineFlags.useDebugColoring()).thenReturn(false) - val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel) + val view = + ModernStatusBarMobileView.constructAndBind( + context, + viewLogger, + SLOT_NAME, + viewModel, + ) ViewUtils.attachView(view) testableLooper.processAllMessages() @@ -214,7 +258,13 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() { @Test fun setStaticDrawableColor_iconHasNewColor() { whenever(statusBarPipelineFlags.useDebugColoring()).thenReturn(false) - val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel) + val view = + ModernStatusBarMobileView.constructAndBind( + context, + viewLogger, + SLOT_NAME, + viewModel, + ) ViewUtils.attachView(view) testableLooper.processAllMessages() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt index f9830309252d..a6d915243f60 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt @@ -28,6 +28,7 @@ import com.android.systemui.statusbar.pipeline.mobile.ui.model.SignalIconModel import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModelTest.Companion.defaultSignal import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn @@ -84,7 +85,7 @@ class LocationBasedMobileIconViewModelTest : SysuiTestCase() { testScope.backgroundScope, ) - homeIcon = HomeMobileIconViewModel(commonImpl, statusBarPipelineFlags) + homeIcon = HomeMobileIconViewModel(commonImpl, statusBarPipelineFlags, mock()) qsIcon = QsMobileIconViewModel(commonImpl, statusBarPipelineFlags) keyguardIcon = KeyguardMobileIconViewModel(commonImpl, statusBarPipelineFlags) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt index 4628f8410245..ddb7f4d88d30 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt @@ -24,6 +24,8 @@ import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirp import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger +import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository @@ -51,6 +53,8 @@ class MobileIconsViewModelTest : SysuiTestCase() { private lateinit var airplaneModeInteractor: AirplaneModeInteractor @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags @Mock private lateinit var constants: ConnectivityConstants + @Mock private lateinit var logger: MobileViewLogger + @Mock private lateinit var verboseLogger: VerboseMobileViewLogger private val testDispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(testDispatcher) @@ -73,6 +77,8 @@ class MobileIconsViewModelTest : SysuiTestCase() { underTest = MobileIconsViewModel( subscriptionIdsFlow, + logger, + verboseLogger, interactor, airplaneModeInteractor, constants, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt index e4c8fd0cd8a1..b4039d906810 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt @@ -164,6 +164,10 @@ class ModernStatusBarViewTest : SysuiTestCase() { override fun getShouldIconBeVisible(): Boolean { return shouldIconBeVisibleInternal } + + override fun isCollecting(): Boolean { + return true + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java index 48b17322da4d..481d453fa0b1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java @@ -38,6 +38,7 @@ import com.android.internal.R; import com.android.internal.view.RotationPolicy; import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManager; import com.android.systemui.SysuiTestCase; +import com.android.systemui.dump.DumpManager; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; import com.android.systemui.util.wrapper.RotationPolicyWrapper; @@ -55,10 +56,12 @@ public class DeviceStateRotationLockSettingControllerTest extends SysuiTestCase private static final String[] DEFAULT_SETTINGS = new String[]{"0:1", "2:0:1", "1:2"}; + @Mock private DeviceStateManager mDeviceStateManager; + @Mock private DeviceStateRotationLockSettingControllerLogger mLogger; + @Mock private DumpManager mDumpManager; + private final FakeSystemClock mFakeSystemClock = new FakeSystemClock(); private final FakeExecutor mFakeExecutor = new FakeExecutor(mFakeSystemClock); - @Mock - private DeviceStateManager mDeviceStateManager; private final RotationPolicyWrapper mFakeRotationPolicy = new FakeRotationPolicy(); private DeviceStateRotationLockSettingController mDeviceStateRotationLockSettingController; private DeviceStateManager.DeviceStateCallback mDeviceStateCallback; @@ -78,7 +81,13 @@ public class DeviceStateRotationLockSettingControllerTest extends SysuiTestCase mSettingsManager = DeviceStateRotationLockSettingsManager.getInstance(mContext); mDeviceStateRotationLockSettingController = new DeviceStateRotationLockSettingController( - mFakeRotationPolicy, mDeviceStateManager, mFakeExecutor, mSettingsManager); + mFakeRotationPolicy, + mDeviceStateManager, + mFakeExecutor, + mSettingsManager, + mLogger, + mDumpManager + ); mDeviceStateRotationLockSettingController.setListening(true); verify(mDeviceStateManager) @@ -173,15 +182,11 @@ public class DeviceStateRotationLockSettingControllerTest extends SysuiTestCase } @Test - public void whenDeviceStateSwitchedToIgnoredState_usePreviousSetting() { - initializeSettingsWith( - 0, DEVICE_STATE_ROTATION_LOCK_IGNORED, 1, DEVICE_STATE_ROTATION_LOCK_UNLOCKED); - mFakeRotationPolicy.setRotationLock(true); - - mDeviceStateCallback.onStateChanged(1); - assertThat(mFakeRotationPolicy.isRotationLocked()).isFalse(); - + public void whenDeviceStateSwitchedToIgnoredState_useFallbackSetting() { mDeviceStateCallback.onStateChanged(0); + assertThat(mFakeRotationPolicy.isRotationLocked()).isTrue(); + + mDeviceStateCallback.onStateChanged(2); assertThat(mFakeRotationPolicy.isRotationLocked()).isFalse(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/RotationChangeProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/RotationChangeProviderTest.kt index 85cfef727954..fd368eb07b5b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/RotationChangeProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/RotationChangeProviderTest.kt @@ -16,22 +16,24 @@ package com.android.systemui.unfold.updates +import android.content.Context +import android.hardware.display.DisplayManager +import android.os.Looper import android.testing.AndroidTestingRunner -import android.view.IRotationWatcher -import android.view.IWindowManager +import android.view.Display import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener -import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.time.FakeSystemClock +import com.android.systemui.util.mockito.whenever +import com.android.systemui.utils.os.FakeHandler import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.any -import org.mockito.ArgumentMatchers.anyInt import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito.spy import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.MockitoAnnotations @@ -42,19 +44,23 @@ class RotationChangeProviderTest : SysuiTestCase() { private lateinit var rotationChangeProvider: RotationChangeProvider - @Mock lateinit var windowManagerInterface: IWindowManager + @Mock lateinit var displayManager: DisplayManager @Mock lateinit var listener: RotationListener - @Captor lateinit var rotationWatcher: ArgumentCaptor<IRotationWatcher> - private val fakeExecutor = FakeExecutor(FakeSystemClock()) + @Mock lateinit var display: Display + @Captor lateinit var displayListener: ArgumentCaptor<DisplayManager.DisplayListener> + private val fakeHandler = FakeHandler(Looper.getMainLooper()) + + private lateinit var spyContext: Context @Before fun setup() { MockitoAnnotations.initMocks(this) - rotationChangeProvider = - RotationChangeProvider(windowManagerInterface, context, fakeExecutor) + spyContext = spy(context) + whenever(spyContext.display).thenReturn(display) + rotationChangeProvider = RotationChangeProvider(displayManager, spyContext, fakeHandler) rotationChangeProvider.addCallback(listener) - fakeExecutor.runAllReady() - verify(windowManagerInterface).watchRotation(rotationWatcher.capture(), anyInt()) + fakeHandler.dispatchQueuedMessages() + verify(displayManager).registerDisplayListener(displayListener.capture(), any()) } @Test @@ -70,15 +76,16 @@ class RotationChangeProviderTest : SysuiTestCase() { verify(listener).onRotationChanged(42) rotationChangeProvider.removeCallback(listener) - fakeExecutor.runAllReady() + fakeHandler.dispatchQueuedMessages() sendRotationUpdate(43) - verify(windowManagerInterface).removeRotationWatcher(any()) + verify(displayManager).unregisterDisplayListener(any()) verifyNoMoreInteractions(listener) } private fun sendRotationUpdate(newRotation: Int) { - rotationWatcher.value.onRotationChanged(newRotation) - fakeExecutor.runAllReady() + whenever(display.rotation).thenReturn(newRotation) + displayListener.allValues.forEach { it.onDisplayChanged(display.displayId) } + fakeHandler.dispatchQueuedMessages() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt index d00acb89d228..3ed6cc88826c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt @@ -29,6 +29,8 @@ import android.os.UserManager import android.provider.Settings import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.GuestResetOrExitSessionReceiver import com.android.systemui.GuestResumeSessionReceiver import com.android.systemui.R @@ -62,6 +64,7 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import junit.framework.Assert.assertNotNull import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher @@ -72,6 +75,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 +import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock @@ -96,6 +100,7 @@ class UserInteractorTest : SysuiTestCase() { @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver @Mock private lateinit var commandQueue: CommandQueue + @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor private lateinit var underTest: UserInteractor @@ -154,6 +159,7 @@ class UserInteractorTest : SysuiTestCase() { repository = telephonyRepository, ), broadcastDispatcher = fakeBroadcastDispatcher, + keyguardUpdateMonitor = keyguardUpdateMonitor, backgroundDispatcher = testDispatcher, activityManager = activityManager, refreshUsersScheduler = refreshUsersScheduler, @@ -177,6 +183,18 @@ class UserInteractorTest : SysuiTestCase() { } @Test + fun `testKeyguardUpdateMonitor_onKeyguardGoingAway`() = + testScope.runTest { + val argumentCaptor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java) + verify(keyguardUpdateMonitor).registerCallback(argumentCaptor.capture()) + + argumentCaptor.value.onKeyguardGoingAway() + + val lastValue = collectLastValue(underTest.dialogDismissRequests) + assertNotNull(lastValue) + } + + @Test fun `onRecordSelected - user`() = testScope.runTest { val userInfos = createUserInfos(count = 3, includeGuest = false) diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt index 22fc32af1b80..daa71b942a2e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt @@ -25,6 +25,7 @@ import android.graphics.drawable.BitmapDrawable import android.os.UserManager import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger +import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.GuestResetOrExitSessionReceiver import com.android.systemui.GuestResumeSessionReceiver import com.android.systemui.SysuiTestCase @@ -80,6 +81,7 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() { @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver @Mock private lateinit var commandQueue: CommandQueue + @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor private lateinit var underTest: StatusBarUserChipViewModel @@ -263,6 +265,7 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() { repository = FakeTelephonyRepository(), ), broadcastDispatcher = fakeBroadcastDispatcher, + keyguardUpdateMonitor = keyguardUpdateMonitor, backgroundDispatcher = testDispatcher, activityManager = activityManager, refreshUsersScheduler = refreshUsersScheduler, diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt index a2bd8d365192..e08ebf4a9050 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt @@ -23,6 +23,7 @@ import android.content.pm.UserInfo import android.os.UserManager import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger +import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.GuestResetOrExitSessionReceiver import com.android.systemui.GuestResumeSessionReceiver import com.android.systemui.SysuiTestCase @@ -81,6 +82,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver @Mock private lateinit var commandQueue: CommandQueue + @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor private lateinit var underTest: UserSwitcherViewModel @@ -165,6 +167,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { repository = FakeTelephonyRepository(), ), broadcastDispatcher = fakeBroadcastDispatcher, + keyguardUpdateMonitor = keyguardUpdateMonitor, backgroundDispatcher = testDispatcher, activityManager = activityManager, refreshUsersScheduler = refreshUsersScheduler, diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java index 31cce4f3168b..468c5a73645b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java @@ -88,7 +88,7 @@ public class ImageWallpaperTest extends SysuiTestCase { @Mock private Bitmap mWallpaperBitmap; FakeSystemClock mFakeSystemClock = new FakeSystemClock(); - FakeExecutor mFakeBackgroundExecutor = new FakeExecutor(mFakeSystemClock); + FakeExecutor mFakeExecutor = new FakeExecutor(mFakeSystemClock); @Before public void setUp() throws Exception { @@ -125,7 +125,7 @@ public class ImageWallpaperTest extends SysuiTestCase { @Test public void testBitmapWallpaper_normal() { - // Will use a image wallpaper with dimensions DISPLAY_WIDTH x DISPLAY_WIDTH. + // Will use an image wallpaper with dimensions DISPLAY_WIDTH x DISPLAY_WIDTH. // Then we expect the surface size will be also DISPLAY_WIDTH x DISPLAY_WIDTH. int bitmapSide = DISPLAY_WIDTH; testSurfaceHelper( @@ -137,7 +137,7 @@ public class ImageWallpaperTest extends SysuiTestCase { @Test public void testBitmapWallpaper_low_resolution() { - // Will use a image wallpaper with dimensions BMP_WIDTH x BMP_HEIGHT. + // Will use an image wallpaper with dimensions BMP_WIDTH x BMP_HEIGHT. // Then we expect the surface size will be also BMP_WIDTH x BMP_HEIGHT. testSurfaceHelper(LOW_BMP_WIDTH /* bitmapWidth */, LOW_BMP_HEIGHT /* bitmapHeight */, @@ -161,13 +161,13 @@ public class ImageWallpaperTest extends SysuiTestCase { ImageWallpaper.CanvasEngine spyEngine = getSpyEngine(); spyEngine.onCreate(mSurfaceHolder); spyEngine.onSurfaceRedrawNeeded(mSurfaceHolder); - assertThat(mFakeBackgroundExecutor.numPending()).isAtLeast(1); + assertThat(mFakeExecutor.numPending()).isAtLeast(1); int n = 0; - while (mFakeBackgroundExecutor.numPending() >= 1) { + while (mFakeExecutor.numPending() >= 1) { n++; assertThat(n).isAtMost(10); - mFakeBackgroundExecutor.runNextReady(); + mFakeExecutor.runNextReady(); mFakeSystemClock.advanceTime(1000); } @@ -176,7 +176,7 @@ public class ImageWallpaperTest extends SysuiTestCase { } private ImageWallpaper createImageWallpaper() { - return new ImageWallpaper(mFakeBackgroundExecutor, mUserTracker) { + return new ImageWallpaper(mFakeExecutor, mUserTracker) { @Override public Engine onCreateEngine() { return new CanvasEngine() { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt new file mode 100644 index 000000000000..4e435462be50 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt @@ -0,0 +1,41 @@ +/* + * 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.keyboard.data.repository + +import com.android.systemui.keyboard.shared.model.BacklightModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull + +class FakeKeyboardRepository : KeyboardRepository { + + private val _keyboardConnected = MutableStateFlow(false) + override val keyboardConnected: Flow<Boolean> = _keyboardConnected + + private val _backlightState: MutableStateFlow<BacklightModel?> = MutableStateFlow(null) + // filtering to make sure backlight doesn't have default initial value + override val backlight: Flow<BacklightModel> = _backlightState.filterNotNull() + + fun setBacklight(state: BacklightModel) { + _backlightState.value = state + } + + fun setKeyboardConnected(connected: Boolean) { + _keyboardConnected.value = connected + } +} diff --git a/packages/SystemUI/unfold/Android.bp b/packages/SystemUI/unfold/Android.bp index 180b611aa13b..2e0a9462ffbe 100644 --- a/packages/SystemUI/unfold/Android.bp +++ b/packages/SystemUI/unfold/Android.bp @@ -35,6 +35,7 @@ android_library { ], kotlincflags: ["-Xjvm-default=enable"], java_version: "1.8", + sdk_version: "current", min_sdk_version: "current", plugins: ["dagger2-compiler"], } diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt index 068347cfe9d8..c3a6cf035d09 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt @@ -19,14 +19,15 @@ package com.android.systemui.unfold import android.content.ContentResolver import android.content.Context import android.hardware.SensorManager +import android.hardware.display.DisplayManager import android.os.Handler -import android.view.IWindowManager import com.android.systemui.unfold.config.UnfoldTransitionConfig import com.android.systemui.unfold.dagger.UnfoldMain import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg import com.android.systemui.unfold.progress.RemoteUnfoldTransitionReceiver import com.android.systemui.unfold.updates.FoldProvider import com.android.systemui.unfold.updates.RotationChangeProvider +import com.android.systemui.unfold.updates.hinge.HingeAngleProvider import com.android.systemui.unfold.updates.screen.ScreenStatusProvider import com.android.systemui.unfold.util.CurrentActivityTypeProvider import com.android.systemui.unfold.util.UnfoldTransitionATracePrefix @@ -61,12 +62,13 @@ interface UnfoldSharedComponent { @BindsInstance @UnfoldMain executor: Executor, @BindsInstance @UnfoldSingleThreadBg singleThreadBgExecutor: Executor, @BindsInstance @UnfoldTransitionATracePrefix tracingTagPrefix: String, - @BindsInstance windowManager: IWindowManager, + @BindsInstance displayManager: DisplayManager, @BindsInstance contentResolver: ContentResolver = context.contentResolver ): UnfoldSharedComponent } val unfoldTransitionProvider: Optional<UnfoldTransitionProgressProvider> + val hingeAngleProvider: HingeAngleProvider val rotationChangeProvider: RotationChangeProvider } @@ -84,8 +86,9 @@ interface RemoteUnfoldSharedComponent { @BindsInstance context: Context, @BindsInstance config: UnfoldTransitionConfig, @BindsInstance @UnfoldMain executor: Executor, + @BindsInstance @UnfoldMain handler: Handler, @BindsInstance @UnfoldSingleThreadBg singleThreadBgExecutor: Executor, - @BindsInstance windowManager: IWindowManager, + @BindsInstance displayManager: DisplayManager, @BindsInstance @UnfoldTransitionATracePrefix tracingTagPrefix: String, ): RemoteUnfoldSharedComponent } diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt index 8eb79df55496..18399194434a 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt @@ -19,8 +19,8 @@ package com.android.systemui.unfold import android.content.Context import android.hardware.SensorManager +import android.hardware.display.DisplayManager import android.os.Handler -import android.view.IWindowManager import com.android.systemui.unfold.config.UnfoldTransitionConfig import com.android.systemui.unfold.updates.FoldProvider import com.android.systemui.unfold.updates.screen.ScreenStatusProvider @@ -47,7 +47,7 @@ fun createUnfoldSharedComponent( mainExecutor: Executor, singleThreadBgExecutor: Executor, tracingTagPrefix: String, - windowManager: IWindowManager, + displayManager: DisplayManager, ): UnfoldSharedComponent = DaggerUnfoldSharedComponent.factory() .create( @@ -61,7 +61,7 @@ fun createUnfoldSharedComponent( mainExecutor, singleThreadBgExecutor, tracingTagPrefix, - windowManager, + displayManager, ) /** @@ -73,16 +73,18 @@ fun createRemoteUnfoldSharedComponent( context: Context, config: UnfoldTransitionConfig, mainExecutor: Executor, + mainHandler: Handler, singleThreadBgExecutor: Executor, tracingTagPrefix: String, - windowManager: IWindowManager, + displayManager: DisplayManager, ): RemoteUnfoldSharedComponent = DaggerRemoteUnfoldSharedComponent.factory() .create( context, config, mainExecutor, + mainHandler, singleThreadBgExecutor, - windowManager, + displayManager, tracingTagPrefix, ) diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt index d19b414cb963..28e493651137 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt @@ -16,7 +16,6 @@ package com.android.systemui.unfold.progress import android.os.Trace -import android.os.Trace.TRACE_TAG_APP import android.util.Log import androidx.dynamicanimation.animation.DynamicAnimation import androidx.dynamicanimation.animation.FloatPropertyCompat @@ -110,7 +109,7 @@ class PhysicsBasedUnfoldTransitionProgressProvider @Inject constructor( if (DEBUG) { Log.d(TAG, "onFoldUpdate = ${update.name()}") - Trace.traceCounter(Trace.TRACE_TAG_APP, "fold_update", update) + Trace.setCounter("fold_update", update.toLong()) } } diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt index 82fd2258120a..d653fc7beff2 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt @@ -119,7 +119,7 @@ constructor( "lastHingeAngle: $lastHingeAngle, " + "lastHingeAngleBeforeTransition: $lastHingeAngleBeforeTransition" ) - Trace.traceCounter(Trace.TRACE_TAG_APP, "hinge_angle", angle.toInt()) + Trace.setCounter( "hinge_angle", angle.toLong()) } val currentDirection = diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt index 0cf8224d3a3f..ce8f1a178d05 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt @@ -17,36 +17,32 @@ package com.android.systemui.unfold.updates import android.content.Context +import android.hardware.display.DisplayManager +import android.os.Handler import android.os.RemoteException -import android.view.IRotationWatcher -import android.view.IWindowManager -import android.view.Surface.Rotation import com.android.systemui.unfold.dagger.UnfoldMain import com.android.systemui.unfold.util.CallbackController -import java.util.concurrent.Executor import javax.inject.Inject /** - * Allows to subscribe to rotation changes. - * - * This is needed as rotation updates from [IWindowManager] are received in a binder thread, while - * most of the times we want them in the main one. Updates are provided for the display associated + * Allows to subscribe to rotation changes. Updates are provided for the display associated * to [context]. */ class RotationChangeProvider @Inject constructor( - private val windowManagerInterface: IWindowManager, + private val displayManager: DisplayManager, private val context: Context, - @UnfoldMain private val mainExecutor: Executor, + @UnfoldMain private val mainHandler: Handler, ) : CallbackController<RotationChangeProvider.RotationListener> { private val listeners = mutableListOf<RotationListener>() - private val rotationWatcher = RotationWatcher() + private val displayListener = RotationDisplayListener() + private var lastRotation: Int? = null override fun addCallback(listener: RotationListener) { - mainExecutor.execute { + mainHandler.post { if (listeners.isEmpty()) { subscribeToRotation() } @@ -55,17 +51,18 @@ constructor( } override fun removeCallback(listener: RotationListener) { - mainExecutor.execute { + mainHandler.post { listeners -= listener if (listeners.isEmpty()) { unsubscribeToRotation() + lastRotation = null } } } private fun subscribeToRotation() { try { - windowManagerInterface.watchRotation(rotationWatcher, context.displayId) + displayManager.registerDisplayListener(displayListener, mainHandler) } catch (e: RemoteException) { throw e.rethrowFromSystemServer() } @@ -73,7 +70,7 @@ constructor( private fun unsubscribeToRotation() { try { - windowManagerInterface.removeRotationWatcher(rotationWatcher) + displayManager.unregisterDisplayListener(displayListener) } catch (e: RemoteException) { throw e.rethrowFromSystemServer() } @@ -82,12 +79,25 @@ constructor( /** Gets notified of rotation changes. */ fun interface RotationListener { /** Called once rotation changes. */ - fun onRotationChanged(@Rotation newRotation: Int) + fun onRotationChanged(newRotation: Int) } - private inner class RotationWatcher : IRotationWatcher.Stub() { - override fun onRotationChanged(rotation: Int) { - mainExecutor.execute { listeners.forEach { it.onRotationChanged(rotation) } } + private inner class RotationDisplayListener : DisplayManager.DisplayListener { + + override fun onDisplayChanged(displayId: Int) { + val display = context.display ?: return + + if (displayId == display.displayId) { + val currentRotation = display.rotation + if (lastRotation == null || lastRotation != currentRotation) { + listeners.forEach { it.onRotationChanged(currentRotation) } + lastRotation = currentRotation + } + } } + + override fun onDisplayAdded(displayId: Int) {} + + override fun onDisplayRemoved(displayId: Int) {} } } diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScaleAwareTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScaleAwareTransitionProgressProvider.kt index 06ca153b694b..ce5c5f91914b 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScaleAwareTransitionProgressProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScaleAwareTransitionProgressProvider.kt @@ -79,10 +79,9 @@ constructor( companion object { fun ContentResolver.areAnimationsEnabled(): Boolean { val animationScale = - Settings.Global.getStringForUser( + Settings.Global.getString( this, Settings.Global.ANIMATOR_DURATION_SCALE, - this.userId ) ?.toFloatOrNull() ?: 1f diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 5dcdbd41c6cc..f0dac2607a4e 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -13117,12 +13117,17 @@ public class ActivityManagerService extends IActivityManager.Stub public Intent registerReceiverWithFeature(IApplicationThread caller, String callerPackage, String callerFeatureId, String receiverId, IIntentReceiver receiver, IntentFilter filter, String permission, int userId, int flags) { + enforceNotIsolatedCaller("registerReceiver"); + // Allow Sandbox process to register only unexported receivers. - if ((flags & Context.RECEIVER_NOT_EXPORTED) != 0) { - enforceNotIsolatedCaller("registerReceiver"); - } else if (mSdkSandboxSettings.isBroadcastReceiverRestrictionsEnforced()) { - enforceNotIsolatedOrSdkSandboxCaller("registerReceiver"); + boolean unexported = (flags & Context.RECEIVER_NOT_EXPORTED) != 0; + if (mSdkSandboxSettings.isBroadcastReceiverRestrictionsEnforced() + && Process.isSdkSandboxUid(Binder.getCallingUid()) + && !unexported) { + throw new SecurityException("SDK sandbox process not allowed to call " + + "registerReceiver"); } + ArrayList<Intent> stickyIntents = null; ProcessRecord callerApp = null; final boolean visibleToInstantApps diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 9f1512825c3a..521f342455d6 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -8396,7 +8396,9 @@ public class AudioService extends IAudioService.Stub if (isMutable()) { // For call stream, align mute only when muted, not when index is set to 0 mVolumeGroupState.mute( - forceMuteState ? mIsMuted : groupIndex == 0 || mIsMuted); + forceMuteState ? mIsMuted : + (groupIndex == 0 && !isCallStream(mStreamType)) + || mIsMuted); } } } diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 0b4bc875fee1..25848002b936 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -881,10 +881,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mAutomaticBrightnessController.stop(); } - if (mScreenOffBrightnessSensorController != null) { - mScreenOffBrightnessSensorController.stop(); - } - if (mBrightnessSetting != null) { mBrightnessSetting.unregisterListener(mBrightnessSettingListener); } @@ -1133,6 +1129,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call if (mScreenOffBrightnessSensorController != null) { mScreenOffBrightnessSensorController.stop(); + mScreenOffBrightnessSensorController = null; } loadScreenOffBrightnessSensor(); int[] sensorValueToLux = mDisplayDeviceConfig.getScreenOffBrightnessSensorValueToLux(); @@ -1250,6 +1247,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/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index ab7292d49c7d..6e342939c95a 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -152,6 +152,13 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.S_V2) static final long REQUEST_LISTENING_MUST_MATCH_PACKAGE = 172251878L; + /** + * @hide + */ + @ChangeId + @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU) + static final long REQUEST_LISTENING_OTHER_USER_NOOP = 242194868L; + private final Context mContext; private final Handler mHandler = new Handler(); @@ -1859,7 +1866,12 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D // Check current user if (userId != currentUser) { - throw new IllegalArgumentException("User " + userId + " is not the current user."); + if (CompatChanges.isChangeEnabled(REQUEST_LISTENING_OTHER_USER_NOOP, callingUid)) { + return; + } else { + throw new IllegalArgumentException( + "User " + userId + " is not the current user."); + } } } if (mBar != null) { 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 9944f127521d..681d5b6e34fb 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -1487,6 +1487,12 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mLastReportedMultiWindowMode = inPictureInPictureMode; ensureActivityConfiguration(0 /* globalChanges */, PRESERVE_WINDOWS, true /* ignoreVisibility */); + if (inPictureInPictureMode && findMainWindow() == null) { + // Prevent malicious app entering PiP without valid WindowState, which can in turn + // result a non-touchable PiP window since the InputConsumer for PiP requires it. + EventLog.writeEvent(0x534e4554, "265293293", -1, ""); + removeImmediately(); + } } } @@ -7727,27 +7733,38 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A /** * Returns the requested {@link Configuration.Orientation} for the current activity. + */ + @Configuration.Orientation + @Override + int getRequestedConfigurationOrientation(boolean forDisplay) { + return getRequestedConfigurationOrientation(forDisplay, getOverrideOrientation()); + } + + /** + * Returns the requested {@link Configuration.Orientation} for the requested + * {@link ActivityInfo.ScreenOrientation}. * - * <p>When The current orientation is set to {@link SCREEN_ORIENTATION_BEHIND} it returns the - * requested orientation for the activity below which is the first activity with an explicit + * <p>When the current screen orientation is set to {@link SCREEN_ORIENTATION_BEHIND} it returns + * the requested orientation for the activity below which is the first activity with an explicit * (different from {@link SCREEN_ORIENTATION_UNSET}) orientation which is not {@link * SCREEN_ORIENTATION_BEHIND}. */ @Configuration.Orientation - @Override - int getRequestedConfigurationOrientation(boolean forDisplay) { + int getRequestedConfigurationOrientation(boolean forDisplay, + @ActivityInfo.ScreenOrientation int requestedOrientation) { if (mLetterboxUiController.hasInheritedOrientation()) { final RootDisplayArea root = getRootDisplayArea(); if (forDisplay && root != null && root.isOrientationDifferentFromDisplay()) { - return ActivityInfo.reverseOrientation( + return reverseConfigurationOrientation( mLetterboxUiController.getInheritedOrientation()); } else { return mLetterboxUiController.getInheritedOrientation(); } } - if (task != null && getOverrideOrientation() == SCREEN_ORIENTATION_BEHIND) { + if (task != null && requestedOrientation == SCREEN_ORIENTATION_BEHIND) { // We use Task here because we want to be consistent with what happens in // multi-window mode where other tasks orientations are ignored. + android.util.Log.d("orientation", "We are here"); final ActivityRecord belowCandidate = task.getActivity( a -> a.canDefineOrientationForActivitiesAbove() /* callback */, this /* boundary */, false /* includeBoundary */, @@ -7756,7 +7773,23 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return belowCandidate.getRequestedConfigurationOrientation(forDisplay); } } - return super.getRequestedConfigurationOrientation(forDisplay); + return super.getRequestedConfigurationOrientation(forDisplay, requestedOrientation); + } + + /** + * Returns the reversed configuration orientation. + * @hide + */ + @Configuration.Orientation + public static int reverseConfigurationOrientation(@Configuration.Orientation int orientation) { + switch (orientation) { + case ORIENTATION_LANDSCAPE: + return ORIENTATION_PORTRAIT; + case ORIENTATION_PORTRAIT: + return ORIENTATION_LANDSCAPE; + default: + return orientation; + } } /** @@ -7802,6 +7835,19 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (mLetterboxUiController.shouldIgnoreRequestedOrientation(requestedOrientation)) { return; } + // This is necessary in order to avoid going into size compat mode when the orientation + // change request comes from the app + if (mWmService.mLetterboxConfiguration + .isSizeCompatModeDisabledAfterOrientationChangeFromApp() + && getRequestedConfigurationOrientation(false, requestedOrientation) + != getRequestedConfigurationOrientation(false /*forDisplay */)) { + // Do not change the requested configuration now, because this will be done when setting + // the orientation below with the new mCompatDisplayInsets + clearSizeCompatModeAttributes(); + } + ProtoLog.v(WM_DEBUG_ORIENTATION, + "Setting requested orientation %s for %s", + ActivityInfo.screenOrientationToString(requestedOrientation), this); setOrientation(requestedOrientation, this); // Push the new configuration to the requested app in case where it's not pushed, e.g. when @@ -8008,9 +8054,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( @@ -8023,19 +8067,28 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mDisplayContent, this, mLetterboxBoundsForFixedOrientationAndAspectRatio); } - @VisibleForTesting - void clearSizeCompatMode() { - final float lastSizeCompatScale = mSizeCompatScale; + private void clearSizeCompatModeAttributes() { mInSizeCompatModeForBounds = false; mSizeCompatScale = 1f; mSizeCompatBounds = null; mCompatDisplayInsets = null; + } + + @VisibleForTesting + void clearSizeCompatMode() { + final float lastSizeCompatScale = mSizeCompatScale; + clearSizeCompatModeAttributes(); if (mSizeCompatScale != lastSizeCompatScale) { forAllWindows(WindowState::updateGlobalScale, false /* traverseTopToBottom */); } - // Clear config override in #updateCompatDisplayInsets(). - onRequestedOverrideConfigurationChanged(EMPTY); + final int activityType = getActivityType(); + final Configuration overrideConfig = getRequestedOverrideConfiguration(); + overrideConfig.unset(); + // Keep the activity type which was set when attaching to a task to prevent leaving it + // undefined. + overrideConfig.windowConfiguration.setActivityType(activityType); + onRequestedOverrideConfigurationChanged(overrideConfig); } @Override @@ -8095,9 +8148,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (isFixedOrientationLetterboxAllowed) { resolveFixedOrientationConfiguration(newParentConfiguration); } - - if (getCompatDisplayInsets() != null) { - resolveSizeCompatModeConfiguration(newParentConfiguration); + final CompatDisplayInsets compatDisplayInsets = getCompatDisplayInsets(); + if (compatDisplayInsets != null) { + resolveSizeCompatModeConfiguration(newParentConfiguration, compatDisplayInsets); } else if (inMultiWindowMode() && !isFixedOrientationLetterboxAllowed) { // We ignore activities' requested orientation in multi-window modes. They may be // taken into consideration in resolveFixedOrientationConfiguration call above. @@ -8114,7 +8167,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A resolveAspectRatioRestriction(newParentConfiguration); } - if (isFixedOrientationLetterboxAllowed || getCompatDisplayInsets() != null + if (isFixedOrientationLetterboxAllowed || compatDisplayInsets != null // In fullscreen, can be letterboxed for aspect ratio. || !inMultiWindowMode()) { updateResolvedBoundsPosition(newParentConfiguration); @@ -8122,7 +8175,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A boolean isIgnoreOrientationRequest = mDisplayContent != null && mDisplayContent.getIgnoreOrientationRequest(); - if (getCompatDisplayInsets() == null + if (compatDisplayInsets == null // for size compat mode set in updateCompatDisplayInsets // Fixed orientation letterboxing is possible on both large screen devices // with ignoreOrientationRequest enabled and on phones in split screen even with @@ -8169,7 +8222,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A info.neverSandboxDisplayApis(sConstrainDisplayApisConfig), info.alwaysSandboxDisplayApis(sConstrainDisplayApisConfig), !matchParentBounds(), - getCompatDisplayInsets() != null, + compatDisplayInsets != null, shouldCreateCompatDisplayInsets()); } resolvedConfig.windowConfiguration.setMaxBounds(mTmpBounds); @@ -8181,7 +8234,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() @@ -8577,7 +8630,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * Resolves consistent screen configuration for orientation and rotation changes without * inheriting the parent bounds. */ - private void resolveSizeCompatModeConfiguration(Configuration newParentConfiguration) { + private void resolveSizeCompatModeConfiguration(Configuration newParentConfiguration, + @NonNull CompatDisplayInsets compatDisplayInsets) { final Configuration resolvedConfig = getResolvedOverrideConfiguration(); final Rect resolvedBounds = resolvedConfig.windowConfiguration.getBounds(); @@ -8598,13 +8652,13 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A ? requestedOrientation // We should use the original orientation of the activity when possible to avoid // forcing the activity in the opposite orientation. - : getCompatDisplayInsets().mOriginalRequestedOrientation != ORIENTATION_UNDEFINED - ? getCompatDisplayInsets().mOriginalRequestedOrientation + : compatDisplayInsets.mOriginalRequestedOrientation != ORIENTATION_UNDEFINED + ? compatDisplayInsets.mOriginalRequestedOrientation : newParentConfiguration.orientation; int rotation = newParentConfiguration.windowConfiguration.getRotation(); final boolean isFixedToUserRotation = mDisplayContent == null || mDisplayContent.getDisplayRotation().isFixedToUserRotation(); - if (!isFixedToUserRotation && !getCompatDisplayInsets().mIsFloating) { + if (!isFixedToUserRotation && !compatDisplayInsets.mIsFloating) { // Use parent rotation because the original display can be rotated. resolvedConfig.windowConfiguration.setRotation(rotation); } else { @@ -8620,11 +8674,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // rely on them to contain the original and unchanging width and height of the app. final Rect containingAppBounds = new Rect(); final Rect containingBounds = mTmpBounds; - getCompatDisplayInsets().getContainerBounds(containingAppBounds, containingBounds, rotation, + compatDisplayInsets.getContainerBounds(containingAppBounds, containingBounds, rotation, orientation, orientationRequested, isFixedToUserRotation); resolvedBounds.set(containingBounds); // The size of floating task is fixed (only swap), so the aspect ratio is already correct. - if (!getCompatDisplayInsets().mIsFloating) { + if (!compatDisplayInsets.mIsFloating) { mIsAspectRatioApplied = applyAspectRatio(resolvedBounds, containingAppBounds, containingBounds); } @@ -8633,7 +8687,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // are calculated in compat container space. The actual position on screen will be applied // later, so the calculation is simpler that doesn't need to involve offset from parent. getTaskFragment().computeConfigResourceOverrides(resolvedConfig, newParentConfiguration, - getCompatDisplayInsets()); + compatDisplayInsets); // Use current screen layout as source because the size of app is independent to parent. resolvedConfig.screenLayout = TaskFragment.computeScreenLayoutOverride( getConfiguration().screenLayout, resolvedConfig.screenWidthDp, diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 491e58b63f76..c527310abb14 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -1486,7 +1486,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { a.persistableMode = ActivityInfo.PERSIST_NEVER; a.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; a.colorMode = ActivityInfo.COLOR_MODE_DEFAULT; - a.flags |= ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS | ActivityInfo.FLAG_NO_HISTORY; + a.flags |= ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS; a.resizeMode = RESIZE_MODE_UNRESIZEABLE; a.configChanges = 0xffffffff; diff --git a/services/core/java/com/android/server/wm/EventLogTags.logtags b/services/core/java/com/android/server/wm/EventLogTags.logtags index 1e5a219e5e52..48ce22e227dd 100644 --- a/services/core/java/com/android/server/wm/EventLogTags.logtags +++ b/services/core/java/com/android/server/wm/EventLogTags.logtags @@ -1,4 +1,4 @@ -# See system/core/logcat/event.logtags for a description of the format of this file. +# See system/logging/logcat/event.logtags for a description of the format of this file. option java_package com.android.server.wm @@ -62,6 +62,10 @@ option java_package com.android.server.wm 31002 wm_task_moved (TaskId|1|5),(ToTop|1),(Index|1) # Task removed with source explanation. 31003 wm_task_removed (TaskId|1|5),(Reason|3) + +# Set the requested orientation of an activity. +31006 wm_set_requested_orientation (Orientation|1|5),(Component Name|3) + # bootanim finished: 31007 wm_boot_animation_done (time|2|3) diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java index fa49a6ba6c2b..37cf5bc95a23 100644 --- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java +++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java @@ -19,6 +19,7 @@ package com.android.server.wm; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.LetterboxConfigurationDeviceConfig.KEY_ALLOW_IGNORE_ORIENTATION_REQUEST; +import static com.android.server.wm.LetterboxConfigurationDeviceConfig.KEY_DISABLE_SIZE_COMPAT_MODE_AFTER_ORIENTATION_CHANGE_FROM_APP; import static com.android.server.wm.LetterboxConfigurationDeviceConfig.KEY_ENABLE_CAMERA_COMPAT_TREATMENT; import static com.android.server.wm.LetterboxConfigurationDeviceConfig.KEY_ENABLE_COMPAT_FAKE_FOCUS; import static com.android.server.wm.LetterboxConfigurationDeviceConfig.KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY; @@ -314,6 +315,10 @@ final class LetterboxConfiguration { mDeviceConfig.updateFlagActiveStatus( /* isActive */ mTranslucentLetterboxingEnabled, /* key */ KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY); + mDeviceConfig.updateFlagActiveStatus( + /* isActive */ true, + /* key */ KEY_DISABLE_SIZE_COMPAT_MODE_AFTER_ORIENTATION_CHANGE_FROM_APP); + mLetterboxConfigurationPersister = letterboxConfigurationPersister; mLetterboxConfigurationPersister.start(); } @@ -327,6 +332,16 @@ final class LetterboxConfiguration { } /** + * Whether size compat mode is disabled after an orientation change request comes from the app. + * This value is controlled via {@link android.provider.DeviceConfig}. + */ + // TODO(b/270356567) Clean up this flag + boolean isSizeCompatModeDisabledAfterOrientationChangeFromApp() { + return mDeviceConfig.getFlag( + KEY_DISABLE_SIZE_COMPAT_MODE_AFTER_ORIENTATION_CHANGE_FROM_APP); + } + + /** * Overrides the aspect ratio of letterbox for fixed orientation. If given value is <= {@link * #MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO}, both it and a value of {@link * com.android.internal.R.dimen.config_fixedOrientationLetterboxAspectRatio} will be ignored and diff --git a/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java b/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java index df3c8f0fdccc..1651af328b1f 100644 --- a/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java +++ b/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java @@ -20,7 +20,6 @@ import android.annotation.NonNull; import android.provider.DeviceConfig; import android.util.ArraySet; - import com.android.internal.annotations.VisibleForTesting; import java.util.Map; @@ -53,6 +52,11 @@ final class LetterboxConfigurationDeviceConfig private static final boolean DEFAULT_VALUE_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY = true; + static final String KEY_DISABLE_SIZE_COMPAT_MODE_AFTER_ORIENTATION_CHANGE_FROM_APP = + "disable_size_compat_mode_after_orientation_change_from_app"; + private static final boolean + DEFAULT_VALUE_DISABLE_SIZE_COMPAT_MODE_AFTER_ORIENTATION_CHANGE_FROM_APP = true; + @VisibleForTesting static final Map<String, Boolean> sKeyToDefaultValueMap = Map.of( KEY_ENABLE_CAMERA_COMPAT_TREATMENT, @@ -64,7 +68,9 @@ final class LetterboxConfigurationDeviceConfig KEY_ENABLE_COMPAT_FAKE_FOCUS, DEFAULT_VALUE_ENABLE_COMPAT_FAKE_FOCUS, KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY, - DEFAULT_VALUE_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY + DEFAULT_VALUE_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY, + KEY_DISABLE_SIZE_COMPAT_MODE_AFTER_ORIENTATION_CHANGE_FROM_APP, + DEFAULT_VALUE_DISABLE_SIZE_COMPAT_MODE_AFTER_ORIENTATION_CHANGE_FROM_APP ); // Whether camera compatibility treatment is enabled. @@ -93,6 +99,10 @@ final class LetterboxConfigurationDeviceConfig private boolean mIsTranslucentLetterboxingAllowed = DEFAULT_VALUE_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY; + // Whether size compat mode is disabled after an orientation change request comes from the app + private boolean mIsSizeCompatModeDisabledAfterOrientationChangeFromApp = + DEFAULT_VALUE_DISABLE_SIZE_COMPAT_MODE_AFTER_ORIENTATION_CHANGE_FROM_APP; + // Set of active device configs that need to be updated in // DeviceConfig.OnPropertiesChangedListener#onPropertiesChanged. private final ArraySet<String> mActiveDeviceConfigsSet = new ArraySet<>(); @@ -142,6 +152,8 @@ final class LetterboxConfigurationDeviceConfig return mIsCompatFakeFocusAllowed; case KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY: return mIsTranslucentLetterboxingAllowed; + case KEY_DISABLE_SIZE_COMPAT_MODE_AFTER_ORIENTATION_CHANGE_FROM_APP: + return mIsSizeCompatModeDisabledAfterOrientationChangeFromApp; default: throw new AssertionError("Unexpected flag name: " + key); } @@ -169,6 +181,10 @@ final class LetterboxConfigurationDeviceConfig case KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY: mIsTranslucentLetterboxingAllowed = getDeviceConfig(key, defaultValue); break; + case KEY_DISABLE_SIZE_COMPAT_MODE_AFTER_ORIENTATION_CHANGE_FROM_APP: + mIsSizeCompatModeDisabledAfterOrientationChangeFromApp = + getDeviceConfig(key, defaultValue); + break; default: throw new AssertionError("Unexpected flag name: " + key); } diff --git a/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java b/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java index 4a99db594755..3b10debaa753 100644 --- a/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java +++ b/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java @@ -216,6 +216,10 @@ class LetterboxConfigurationPersister { } private void readCurrentConfiguration() { + if (!mConfigurationFile.exists()) { + useDefaultValue(); + return; + } FileInputStream fis = null; try { fis = mConfigurationFile.openRead(); 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..7ff92afc9d29 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,9 +1454,28 @@ 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(); + return getRequestedConfigurationOrientation(forDisplay, getOverrideOrientation()); + } + + /** + * Gets the configuration orientation by the requested screen orientation + * + * @param forDisplay whether it is the requested config orientation for display. + * If {@code true}, we may reverse the requested orientation if the root is + * different from the display, so that when the display rotates to the + * reversed orientation, the requested app will be in the requested + * orientation. + * @param requestedOrientation the screen orientation({@link ScreenOrientation}) that is + * requested + * @return orientation in ({@link Configuration#ORIENTATION_LANDSCAPE}, + * {@link Configuration#ORIENTATION_PORTRAIT}, + * {@link Configuration#ORIENTATION_UNDEFINED}). + */ + @Configuration.Orientation + int getRequestedConfigurationOrientation(boolean forDisplay, + @ScreenOrientation int requestedOrientation) { final RootDisplayArea root = getRootDisplayArea(); if (forDisplay && root != null && root.isOrientationDifferentFromDisplay()) { // Reverse the requested orientation if the orientation of its root is different from @@ -1466,7 +1485,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< // (portrait). // When an app below the DAG is requesting landscape, it should actually request the // display to be portrait, so that the DAG and the app will be in landscape. - requestedOrientation = reverseOrientation(getOverrideOrientation()); + requestedOrientation = reverseOrientation(requestedOrientation); } if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_NOSENSOR) { diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 8931d8030df2..e6c1e75da581 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -6198,9 +6198,10 @@ public class WindowManagerService extends IWindowManager.Stub waitingForConfig = waitingForRemoteDisplayChange = false; numOpeningApps = 0; } - if (waitingForConfig || waitingForRemoteDisplayChange || mAppsFreezingScreen > 0 - || mWindowsFreezingScreen == WINDOWS_FREEZING_SCREENS_ACTIVE - || mClientFreezingScreen || numOpeningApps > 0) { + final boolean waitingForApps = mWindowsFreezingScreen != WINDOWS_FREEZING_SCREENS_TIMEOUT + && (mAppsFreezingScreen > 0 || numOpeningApps > 0); + if (waitingForConfig || waitingForRemoteDisplayChange || waitingForApps + || mClientFreezingScreen) { ProtoLog.d(WM_DEBUG_ORIENTATION, "stopFreezingDisplayLocked: Returning " + "waitingForConfig=%b, waitingForRemoteDisplayChange=%b, " + "mAppsFreezingScreen=%d, mWindowsFreezingScreen=%d, " diff --git a/services/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) { diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index ea50179746d8..0300eb06c220 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -2377,7 +2377,7 @@ public class ActivityRecordTests extends WindowTestsBase { .setScreenOrientation(SCREEN_ORIENTATION_BEHIND) .build(); final int topOrientation = activityTop.getRequestedConfigurationOrientation(); - assertEquals(SCREEN_ORIENTATION_PORTRAIT, topOrientation); + assertEquals(ORIENTATION_PORTRAIT, topOrientation); } private void verifyProcessInfoUpdate(ActivityRecord activity, State state, diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 117805bf344d..d77b6ada268e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -381,7 +381,7 @@ public class SizeCompatTests extends WindowTestsBase { } @Test - public void testTranslucentActivitiesDontGoInSizeCompactMode() { + public void testTranslucentActivitiesDontGoInSizeCompatMode() { mWm.mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(true); setUpDisplaySizeWithApp(2800, 1400); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); @@ -2454,11 +2454,11 @@ public class SizeCompatTests extends WindowTestsBase { assertFalse(mActivity.inSizeCompatMode()); mActivity.setRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED); - - assertTrue(mActivity.inSizeCompatMode()); - // We should remember the original orientation. + // Activity is not in size compat mode because the orientation change request came from the + // app itself + assertFalse(mActivity.inSizeCompatMode()); assertEquals(mActivity.getResolvedOverrideConfiguration().orientation, - Configuration.ORIENTATION_PORTRAIT); + Configuration.ORIENTATION_UNDEFINED); } @Test @@ -3033,6 +3033,25 @@ public class SizeCompatTests extends WindowTestsBase { } @Test + public void testAppRequestsOrientationChange_notInSizeCompat() { + setUpDisplaySizeWithApp(2200, 1800); + mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); + + prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE); + + mActivity.setRequestedOrientation(SCREEN_ORIENTATION_PORTRAIT); + + // Activity is not in size compat mode because the orientation change request came from the + // app itself + assertFalse(mActivity.inSizeCompatMode()); + + rotateDisplay(mActivity.mDisplayContent, ROTATION_270); + // Activity should go into size compat mode now because the orientation change came from the + // system (device rotation) + assertTrue(mActivity.inSizeCompatMode()); + } + + @Test public void testLetterboxDetailsForStatusBar_noLetterbox() { setUpDisplaySizeWithApp(2800, 1000); addStatusBar(mActivity.mDisplayContent); diff --git a/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java index 3db011683a86..fdd919412e55 100644 --- a/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java +++ b/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java @@ -86,7 +86,7 @@ public class ProtoLogImplTest { mFile = testContext.getFileStreamPath("tracing_test.dat"); //noinspection ResultOfMethodCallIgnored mFile.delete(); - mProtoLog = new ProtoLogImpl(mFile, 1024 * 1024, mReader); + mProtoLog = new ProtoLogImpl(mFile, 1024 * 1024, mReader, 1024); } @After |