diff options
110 files changed, 3238 insertions, 695 deletions
diff --git a/core/java/android/service/controls/OWNERS b/core/java/android/service/controls/OWNERS new file mode 100644 index 000000000000..4bb78c731137 --- /dev/null +++ b/core/java/android/service/controls/OWNERS @@ -0,0 +1,4 @@ +# Bug component: 802726 +asc@google.com +kozynski@google.com +juliacr@google.com
\ No newline at end of file diff --git a/core/java/android/service/notification/ZenPolicy.java b/core/java/android/service/notification/ZenPolicy.java index a892570f589c..bffa660cdbd9 100644 --- a/core/java/android/service/notification/ZenPolicy.java +++ b/core/java/android/service/notification/ZenPolicy.java @@ -79,6 +79,12 @@ public final class ZenPolicy implements Parcelable { /** @hide */ public static final int PRIORITY_CATEGORY_CONVERSATIONS = 8; + /** + * Total number of priority categories. Keep updated with any updates to PriorityCategory enum. + * @hide + */ + public static final int NUM_PRIORITY_CATEGORIES = 9; + /** @hide */ @IntDef(prefix = { "VISUAL_EFFECT_" }, value = { VISUAL_EFFECT_FULL_SCREEN_INTENT, @@ -107,6 +113,12 @@ public final class ZenPolicy implements Parcelable { /** @hide */ public static final int VISUAL_EFFECT_NOTIFICATION_LIST = 6; + /** + * Total number of visual effects. Keep updated with any updates to VisualEffect enum. + * @hide + */ + public static final int NUM_VISUAL_EFFECTS = 7; + /** @hide */ @IntDef(prefix = { "PEOPLE_TYPE_" }, value = { PEOPLE_TYPE_UNSET, @@ -202,8 +214,8 @@ public final class ZenPolicy implements Parcelable { /** @hide */ public ZenPolicy() { - mPriorityCategories = new ArrayList<>(Collections.nCopies(9, 0)); - mVisualEffects = new ArrayList<>(Collections.nCopies(7, 0)); + mPriorityCategories = new ArrayList<>(Collections.nCopies(NUM_PRIORITY_CATEGORIES, 0)); + mVisualEffects = new ArrayList<>(Collections.nCopies(NUM_VISUAL_EFFECTS, 0)); } /** @@ -804,8 +816,12 @@ public final class ZenPolicy implements Parcelable { @Override public ZenPolicy createFromParcel(Parcel source) { ZenPolicy policy = new ZenPolicy(); - policy.mPriorityCategories = source.readArrayList(Integer.class.getClassLoader(), java.lang.Integer.class); - policy.mVisualEffects = source.readArrayList(Integer.class.getClassLoader(), java.lang.Integer.class); + policy.mPriorityCategories = trimList( + source.readArrayList(Integer.class.getClassLoader(), java.lang.Integer.class), + NUM_PRIORITY_CATEGORIES); + policy.mVisualEffects = trimList( + source.readArrayList(Integer.class.getClassLoader(), java.lang.Integer.class), + NUM_VISUAL_EFFECTS); policy.mPriorityCalls = source.readInt(); policy.mPriorityMessages = source.readInt(); policy.mConversationSenders = source.readInt(); @@ -832,6 +848,15 @@ public final class ZenPolicy implements Parcelable { .toString(); } + // Returns a list containing the first maxLength elements of the input list if the list is + // longer than that size. For the lists in ZenPolicy, this should not happen unless the input + // is corrupt. + private static ArrayList<Integer> trimList(ArrayList<Integer> list, int maxLength) { + if (list == null || list.size() <= maxLength) { + return list; + } + return new ArrayList<>(list.subList(0, maxLength)); + } private String priorityCategoriesToString() { StringBuilder builder = new StringBuilder(); diff --git a/core/java/android/window/ImeOnBackInvokedDispatcher.java b/core/java/android/window/ImeOnBackInvokedDispatcher.java index 14f52282b3aa..9152e7837b82 100644 --- a/core/java/android/window/ImeOnBackInvokedDispatcher.java +++ b/core/java/android/window/ImeOnBackInvokedDispatcher.java @@ -123,7 +123,7 @@ public class ImeOnBackInvokedDispatcher implements OnBackInvokedDispatcher, Parc private void receive( int resultCode, Bundle resultData, - @NonNull OnBackInvokedDispatcher receivingDispatcher) { + @NonNull WindowOnBackInvokedDispatcher receivingDispatcher) { final int callbackId = resultData.getInt(RESULT_KEY_ID); if (resultCode == RESULT_CODE_REGISTER) { int priority = resultData.getInt(RESULT_KEY_PRIORITY); @@ -140,11 +140,11 @@ public class ImeOnBackInvokedDispatcher implements OnBackInvokedDispatcher, Parc @NonNull IOnBackInvokedCallback iCallback, @OnBackInvokedDispatcher.Priority int priority, int callbackId, - @NonNull OnBackInvokedDispatcher receivingDispatcher) { + @NonNull WindowOnBackInvokedDispatcher receivingDispatcher) { final ImeOnBackInvokedCallback imeCallback = new ImeOnBackInvokedCallback(iCallback, callbackId, priority); mImeCallbacks.add(imeCallback); - receivingDispatcher.registerOnBackInvokedCallback(priority, imeCallback); + receivingDispatcher.registerOnBackInvokedCallbackUnchecked(imeCallback, priority); } private void unregisterReceivedCallback( diff --git a/core/tests/coretests/src/android/service/controls/OWNERS b/core/tests/coretests/src/android/service/controls/OWNERS new file mode 100644 index 000000000000..bf67034abf8a --- /dev/null +++ b/core/tests/coretests/src/android/service/controls/OWNERS @@ -0,0 +1 @@ +include platform/frameworks/base:/core/java/android/service/controls/OWNERS
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java index 6b59e313b01b..d7cb490ed0cb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java @@ -16,8 +16,6 @@ package com.android.wm.shell.unfold; -import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; - import android.annotation.NonNull; import android.app.ActivityManager.RunningTaskInfo; import android.app.TaskInfo; @@ -56,6 +54,12 @@ public class UnfoldAnimationController implements UnfoldListener { private final SparseArray<SurfaceControl> mTaskSurfaces = new SparseArray<>(); private final SparseArray<UnfoldTaskAnimator> mAnimatorsByTaskId = new SparseArray<>(); + /** + * Indicates whether we're in stage change process. This should be set to {@code true} in + * {@link #onStateChangeStarted()} and {@code false} in {@link #onStateChangeFinished()}. + */ + private boolean mIsInStageChange; + public UnfoldAnimationController( @NonNull ShellInit shellInit, @NonNull TransactionPool transactionPool, @@ -123,7 +127,7 @@ public class UnfoldAnimationController implements UnfoldListener { animator.onTaskChanged(taskInfo); } else { // Became inapplicable - resetTask(animator, taskInfo); + maybeResetTask(animator, taskInfo); animator.onTaskVanished(taskInfo); mAnimatorsByTaskId.remove(taskInfo.taskId); } @@ -154,7 +158,7 @@ public class UnfoldAnimationController implements UnfoldListener { final boolean isCurrentlyApplicable = animator != null; if (isCurrentlyApplicable) { - resetTask(animator, taskInfo); + maybeResetTask(animator, taskInfo); animator.onTaskVanished(taskInfo); mAnimatorsByTaskId.remove(taskInfo.taskId); } @@ -166,6 +170,7 @@ public class UnfoldAnimationController implements UnfoldListener { return; } + mIsInStageChange = true; SurfaceControl.Transaction transaction = null; for (int i = 0; i < mAnimators.size(); i++) { final UnfoldTaskAnimator animator = mAnimators.get(i); @@ -219,11 +224,12 @@ public class UnfoldAnimationController implements UnfoldListener { transaction.apply(); mTransactionPool.release(transaction); + mIsInStageChange = false; } - private void resetTask(UnfoldTaskAnimator animator, TaskInfo taskInfo) { - if (taskInfo.getWindowingMode() == WINDOWING_MODE_PINNED) { - // PiP task has its own cleanup path, ignore surface reset to avoid conflict. + private void maybeResetTask(UnfoldTaskAnimator animator, TaskInfo taskInfo) { + if (!mIsInStageChange) { + // No need to resetTask if there is no ongoing state change. return; } final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index f37d22146b25..584823746662 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1275,6 +1275,9 @@ <!-- DREAMING -> LOCKSCREEN transition: Amount to shift lockscreen content on entering --> <dimen name="dreaming_to_lockscreen_transition_lockscreen_translation_y">40dp</dimen> + <!-- OCCLUDED -> LOCKSCREEN transition: Amount to shift lockscreen content on entering --> + <dimen name="occluded_to_lockscreen_transition_lockscreen_translation_y">40dp</dimen> + <!-- The amount of vertical offset for the keyguard during the full shade transition. --> <dimen name="lockscreen_shade_keyguard_transition_vertical_offset">0dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 7a8ffa1863f6..29369cd33cd0 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2751,7 +2751,7 @@ Error message shown when a button should be pressed and held to activate it, usually shown when the user attempted to tap the button or held it for too short a time. [CHAR LIMIT=32]. --> - <string name="keyguard_affordance_press_too_short">Press and hold to activate</string> + <string name="keyguard_affordance_press_too_short">Touch & hold to open</string> <!-- Text for education page of cancel button to hide the page. [CHAR_LIMIT=NONE] --> <string name="rear_display_bottom_sheet_cancel">Cancel</string> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 2c3d3a0e0a64..93027c1914ee 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -884,7 +884,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab Assert.isMainThread(); if (mWakeOnFingerprintAcquiredStart && acquireInfo == FINGERPRINT_ACQUIRED_START) { mPowerManager.wakeUp( - SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE, + SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_BIOMETRIC, "com.android.systemui.keyguard:FINGERPRINT_ACQUIRED_START"); } for (int i = 0; i < mCallbacks.size(); i++) { diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java index 82e570438dab..805a20a6d965 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java @@ -16,15 +16,21 @@ package com.android.systemui.clipboardoverlay; +import static android.content.ClipDescription.CLASSIFICATION_COMPLETE; + import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_ENABLED; import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED; import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_TOAST_SHOWN; + +import static com.google.android.setupcompat.util.WizardManagerHelper.SETTINGS_SECURE_USER_SETUP_COMPLETE; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.os.SystemProperties; import android.provider.DeviceConfig; +import android.provider.Settings; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; @@ -56,6 +62,7 @@ public class ClipboardListener implements private final DeviceConfigProxy mDeviceConfig; private final Provider<ClipboardOverlayController> mOverlayProvider; private final ClipboardOverlayControllerLegacyFactory mOverlayFactory; + private final ClipboardToast mClipboardToast; private final ClipboardManager mClipboardManager; private final UiEventLogger mUiEventLogger; private final FeatureFlags mFeatureFlags; @@ -66,6 +73,7 @@ public class ClipboardListener implements public ClipboardListener(Context context, DeviceConfigProxy deviceConfigProxy, Provider<ClipboardOverlayController> clipboardOverlayControllerProvider, ClipboardOverlayControllerLegacyFactory overlayFactory, + ClipboardToast clipboardToast, ClipboardManager clipboardManager, UiEventLogger uiEventLogger, FeatureFlags featureFlags) { @@ -73,6 +81,7 @@ public class ClipboardListener implements mDeviceConfig = deviceConfigProxy; mOverlayProvider = clipboardOverlayControllerProvider; mOverlayFactory = overlayFactory; + mClipboardToast = clipboardToast; mClipboardManager = clipboardManager; mUiEventLogger = uiEventLogger; mFeatureFlags = featureFlags; @@ -102,6 +111,15 @@ public class ClipboardListener implements return; } + if (!isUserSetupComplete()) { + // just show a toast, user should not access intents from this state + if (shouldShowToast(clipData)) { + mUiEventLogger.log(CLIPBOARD_TOAST_SHOWN, 0, clipSource); + mClipboardToast.showCopiedToast(); + } + return; + } + boolean enabled = mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR); if (mClipboardOverlay == null || enabled != mUsingNewOverlay) { mUsingNewOverlay = enabled; @@ -136,10 +154,26 @@ public class ClipboardListener implements return clipData.getDescription().getExtras().getBoolean(EXTRA_SUPPRESS_OVERLAY, false); } + boolean shouldShowToast(ClipData clipData) { + if (clipData == null) { + return false; + } else if (clipData.getDescription().getClassificationStatus() == CLASSIFICATION_COMPLETE) { + // only show for classification complete if we aren't already showing a toast, to ignore + // the duplicate ClipData with classification + return !mClipboardToast.isShowing(); + } + return true; + } + private static boolean isEmulator() { return SystemProperties.getBoolean("ro.boot.qemu", false); } + private boolean isUserSetupComplete() { + return Settings.Secure.getInt(mContext.getContentResolver(), + SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; + } + interface ClipboardOverlay { void setClipData(ClipData clipData, String clipSource); diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEvent.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEvent.java index 9917507ec3bf..4b5f8765c4d0 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEvent.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEvent.java @@ -43,7 +43,9 @@ public enum ClipboardOverlayEvent implements UiEventLogger.UiEventEnum { @UiEvent(doc = "clipboard overlay tapped outside") CLIPBOARD_OVERLAY_TAP_OUTSIDE(1077), @UiEvent(doc = "clipboard overlay dismissed, miscellaneous reason") - CLIPBOARD_OVERLAY_DISMISSED_OTHER(1078); + CLIPBOARD_OVERLAY_DISMISSED_OTHER(1078), + @UiEvent(doc = "clipboard toast shown") + CLIPBOARD_TOAST_SHOWN(1270); private final int mId; diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardToast.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardToast.java new file mode 100644 index 000000000000..0ed7d2711c62 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardToast.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.clipboardoverlay; + +import android.content.Context; +import android.widget.Toast; + +import com.android.systemui.R; + +import javax.inject.Inject; + +/** + * Utility class for showing a simple clipboard toast on copy. + */ +class ClipboardToast extends Toast.Callback { + private final Context mContext; + private Toast mCopiedToast; + + @Inject + ClipboardToast(Context context) { + mContext = context; + } + + void showCopiedToast() { + if (mCopiedToast != null) { + mCopiedToast.cancel(); + } + mCopiedToast = Toast.makeText(mContext, + R.string.clipboard_overlay_text_copied, Toast.LENGTH_SHORT); + mCopiedToast.show(); + } + + boolean isShowing() { + return mCopiedToast != null; + } + + @Override // Toast.Callback + public void onToastHidden() { + super.onToastHidden(); + mCopiedToast = null; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt b/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt index 16f415070b45..c746efdbbd30 100644 --- a/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt @@ -20,7 +20,7 @@ import android.content.Context import android.database.ContentObserver import android.os.Handler import android.os.Looper -import android.provider.Settings +import com.android.systemui.util.settings.GlobalSettings /** * Class to track the availability of [DemoMode]. Use this class to track the availability and @@ -29,7 +29,10 @@ import android.provider.Settings * This class works by wrapping a content observer for the relevant keys related to DemoMode * availability and current on/off state, and triggering callbacks. */ -abstract class DemoModeAvailabilityTracker(val context: Context) { +abstract class DemoModeAvailabilityTracker( + val context: Context, + val globalSettings: GlobalSettings, +) { var isInDemoMode = false var isDemoModeAvailable = false @@ -41,9 +44,9 @@ abstract class DemoModeAvailabilityTracker(val context: Context) { fun startTracking() { val resolver = context.contentResolver resolver.registerContentObserver( - Settings.Global.getUriFor(DEMO_MODE_ALLOWED), false, allowedObserver) + globalSettings.getUriFor(DEMO_MODE_ALLOWED), false, allowedObserver) resolver.registerContentObserver( - Settings.Global.getUriFor(DEMO_MODE_ON), false, onObserver) + globalSettings.getUriFor(DEMO_MODE_ON), false, onObserver) } fun stopTracking() { @@ -57,12 +60,11 @@ abstract class DemoModeAvailabilityTracker(val context: Context) { abstract fun onDemoModeFinished() private fun checkIsDemoModeAllowed(): Boolean { - return Settings.Global - .getInt(context.contentResolver, DEMO_MODE_ALLOWED, 0) != 0 + return globalSettings.getInt(DEMO_MODE_ALLOWED, 0) != 0 } private fun checkIsDemoModeOn(): Boolean { - return Settings.Global.getInt(context.contentResolver, DEMO_MODE_ON, 0) != 0 + return globalSettings.getInt(DEMO_MODE_ON, 0) != 0 } private val allowedObserver = object : ContentObserver(Handler(Looper.getMainLooper())) { diff --git a/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt b/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt index 000bbe6afc50..84f83f1ae956 100644 --- a/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt +++ b/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt @@ -24,22 +24,28 @@ import android.os.Bundle import android.os.UserHandle import android.util.Log import com.android.systemui.Dumpable +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.demomode.DemoMode.ACTION_DEMO import com.android.systemui.dump.DumpManager import com.android.systemui.statusbar.policy.CallbackController import com.android.systemui.util.Assert import com.android.systemui.util.settings.GlobalSettings import java.io.PrintWriter +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow /** * Handles system broadcasts for [DemoMode] * * Injected via [DemoModeModule] */ -class DemoModeController constructor( +class DemoModeController +constructor( private val context: Context, private val dumpManager: DumpManager, - private val globalSettings: GlobalSettings + private val globalSettings: GlobalSettings, + private val broadcastDispatcher: BroadcastDispatcher, ) : CallbackController<DemoMode>, Dumpable { // Var updated when the availability tracker changes, or when we enter/exit demo mode in-process @@ -58,9 +64,7 @@ class DemoModeController constructor( requestFinishDemoMode() val m = mutableMapOf<String, MutableList<DemoMode>>() - DemoMode.COMMANDS.map { command -> - m.put(command, mutableListOf()) - } + DemoMode.COMMANDS.map { command -> m.put(command, mutableListOf()) } receiverMap = m } @@ -71,7 +75,7 @@ class DemoModeController constructor( initialized = true - dumpManager.registerDumpable(TAG, this) + dumpManager.registerNormalDumpable(TAG, this) // Due to DemoModeFragment running in systemui:tuner process, we have to observe for // content changes to know if the setting turned on or off @@ -81,8 +85,13 @@ class DemoModeController constructor( val demoFilter = IntentFilter() demoFilter.addAction(ACTION_DEMO) - context.registerReceiverAsUser(broadcastReceiver, UserHandle.ALL, demoFilter, - android.Manifest.permission.DUMP, null, Context.RECEIVER_EXPORTED) + + broadcastDispatcher.registerReceiver( + receiver = broadcastReceiver, + filter = demoFilter, + user = UserHandle.ALL, + permission = android.Manifest.permission.DUMP, + ) } override fun addCallback(listener: DemoMode) { @@ -91,16 +100,15 @@ class DemoModeController constructor( commands.forEach { command -> if (!receiverMap.containsKey(command)) { - throw IllegalStateException("Command ($command) not recognized. " + - "See DemoMode.java for valid commands") + throw IllegalStateException( + "Command ($command) not recognized. " + "See DemoMode.java for valid commands" + ) } receiverMap[command]!!.add(listener) } - synchronized(this) { - receivers.add(listener) - } + synchronized(this) { receivers.add(listener) } if (isInDemoMode) { listener.onDemoModeStarted() @@ -109,14 +117,46 @@ class DemoModeController constructor( override fun removeCallback(listener: DemoMode) { synchronized(this) { - listener.demoCommands().forEach { command -> - receiverMap[command]!!.remove(listener) - } + listener.demoCommands().forEach { command -> receiverMap[command]!!.remove(listener) } receivers.remove(listener) } } + /** + * Create a [Flow] for the stream of demo mode arguments that come in for the given [command] + * + * This is equivalent of creating a listener manually and adding an event handler for the given + * command, like so: + * + * ``` + * class Demoable { + * private val demoHandler = object : DemoMode { + * override fun demoCommands() = listOf(<command>) + * + * override fun dispatchDemoCommand(command: String, args: Bundle) { + * handleDemoCommand(args) + * } + * } + * } + * ``` + * + * @param command The top-level demo mode command you want a stream for + */ + fun demoFlowForCommand(command: String): Flow<Bundle> = conflatedCallbackFlow { + val callback = + object : DemoMode { + override fun demoCommands(): List<String> = listOf(command) + + override fun dispatchDemoCommand(command: String, args: Bundle) { + trySend(args) + } + } + + addCallback(callback) + awaitClose { removeCallback(callback) } + } + private fun setIsDemoModeAllowed(enabled: Boolean) { // Turn off demo mode if it was on if (isInDemoMode && !enabled) { @@ -129,13 +169,9 @@ class DemoModeController constructor( Assert.isMainThread() val copy: List<DemoModeCommandReceiver> - synchronized(this) { - copy = receivers.toList() - } + synchronized(this) { copy = receivers.toList() } - copy.forEach { r -> - r.onDemoModeStarted() - } + copy.forEach { r -> r.onDemoModeStarted() } } private fun exitDemoMode() { @@ -143,18 +179,13 @@ class DemoModeController constructor( Assert.isMainThread() val copy: List<DemoModeCommandReceiver> - synchronized(this) { - copy = receivers.toList() - } + synchronized(this) { copy = receivers.toList() } - copy.forEach { r -> - r.onDemoModeFinished() - } + copy.forEach { r -> r.onDemoModeFinished() } } fun dispatchDemoCommand(command: String, args: Bundle) { Assert.isMainThread() - if (DEBUG) { Log.d(TAG, "dispatchDemoCommand: $command, args=$args") } @@ -172,9 +203,7 @@ class DemoModeController constructor( } // See? demo mode is easy now, you just notify the listeners when their command is called - receiverMap[command]!!.forEach { receiver -> - receiver.dispatchDemoCommand(command, args) - } + receiverMap[command]!!.forEach { receiver -> receiver.dispatchDemoCommand(command, args) } } override fun dump(pw: PrintWriter, args: Array<out String>) { @@ -183,65 +212,64 @@ class DemoModeController constructor( pw.println(" isDemoModeAllowed=$isAvailable") pw.print(" receivers=[") val copy: List<DemoModeCommandReceiver> - synchronized(this) { - copy = receivers.toList() - } - copy.forEach { recv -> - pw.print(" ${recv.javaClass.simpleName}") - } + synchronized(this) { copy = receivers.toList() } + copy.forEach { recv -> pw.print(" ${recv.javaClass.simpleName}") } pw.println(" ]") pw.println(" receiverMap= [") receiverMap.keys.forEach { command -> pw.print(" $command : [") - val recvs = receiverMap[command]!!.map { receiver -> - receiver.javaClass.simpleName - }.joinToString(",") + val recvs = + receiverMap[command]!! + .map { receiver -> receiver.javaClass.simpleName } + .joinToString(",") pw.println("$recvs ]") } } - private val tracker = object : DemoModeAvailabilityTracker(context) { - override fun onDemoModeAvailabilityChanged() { - setIsDemoModeAllowed(isDemoModeAvailable) - } - - override fun onDemoModeStarted() { - if (this@DemoModeController.isInDemoMode != isInDemoMode) { - enterDemoMode() + private val tracker = + object : DemoModeAvailabilityTracker(context, globalSettings) { + override fun onDemoModeAvailabilityChanged() { + setIsDemoModeAllowed(isDemoModeAvailable) } - } - override fun onDemoModeFinished() { - if (this@DemoModeController.isInDemoMode != isInDemoMode) { - exitDemoMode() + override fun onDemoModeStarted() { + if (this@DemoModeController.isInDemoMode != isInDemoMode) { + enterDemoMode() + } } - } - } - private val broadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (DEBUG) { - Log.v(TAG, "onReceive: $intent") - } - - val action = intent.action - if (!ACTION_DEMO.equals(action)) { - return - } - - val bundle = intent.extras ?: return - val command = bundle.getString("command", "").trim().toLowerCase() - if (command.length == 0) { - return + override fun onDemoModeFinished() { + if (this@DemoModeController.isInDemoMode != isInDemoMode) { + exitDemoMode() + } } + } - try { - dispatchDemoCommand(command, bundle) - } catch (t: Throwable) { - Log.w(TAG, "Error running demo command, intent=$intent $t") + private val broadcastReceiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (DEBUG) { + Log.v(TAG, "onReceive: $intent") + } + + val action = intent.action + if (!ACTION_DEMO.equals(action)) { + return + } + + val bundle = intent.extras ?: return + val command = bundle.getString("command", "").trim().lowercase() + if (command.isEmpty()) { + return + } + + try { + dispatchDemoCommand(command, bundle) + } catch (t: Throwable) { + Log.w(TAG, "Error running demo command, intent=$intent $t") + } } } - } fun requestSetDemoModeAllowed(allowed: Boolean) { setGlobal(DEMO_MODE_ALLOWED, if (allowed) 1 else 0) @@ -258,10 +286,12 @@ class DemoModeController constructor( private fun setGlobal(key: String, value: Int) { globalSettings.putInt(key, value) } + + companion object { + const val DEMO_MODE_ALLOWED = "sysui_demo_allowed" + const val DEMO_MODE_ON = "sysui_tuner_demo_on" + } } private const val TAG = "DemoModeController" -private const val DEMO_MODE_ALLOWED = "sysui_demo_allowed" -private const val DEMO_MODE_ON = "sysui_tuner_demo_on" - private const val DEBUG = false diff --git a/packages/SystemUI/src/com/android/systemui/demomode/dagger/DemoModeModule.java b/packages/SystemUI/src/com/android/systemui/demomode/dagger/DemoModeModule.java index de9affb5c748..b84fa5af4d8d 100644 --- a/packages/SystemUI/src/com/android/systemui/demomode/dagger/DemoModeModule.java +++ b/packages/SystemUI/src/com/android/systemui/demomode/dagger/DemoModeModule.java @@ -18,6 +18,7 @@ package com.android.systemui.demomode.dagger; import android.content.Context; +import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dump.DumpManager; @@ -37,8 +38,14 @@ public abstract class DemoModeModule { static DemoModeController provideDemoModeController( Context context, DumpManager dumpManager, - GlobalSettings globalSettings) { - DemoModeController dmc = new DemoModeController(context, dumpManager, globalSettings); + GlobalSettings globalSettings, + BroadcastDispatcher broadcastDispatcher + ) { + DemoModeController dmc = new DemoModeController( + context, + dumpManager, + globalSettings, + broadcastDispatcher); dmc.initialize(); return /*run*/dmc; } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java index 5d21349fce11..5b90ef2bb806 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java @@ -16,7 +16,14 @@ package com.android.systemui.doze; +import static android.os.PowerManager.WAKE_REASON_BIOMETRIC; +import static android.os.PowerManager.WAKE_REASON_GESTURE; +import static android.os.PowerManager.WAKE_REASON_LIFT; +import static android.os.PowerManager.WAKE_REASON_PLUGGED_IN; +import static android.os.PowerManager.WAKE_REASON_TAP; + import android.annotation.IntDef; +import android.os.PowerManager; import android.util.TimeUtils; import androidx.annotation.NonNull; @@ -511,6 +518,25 @@ public class DozeLog implements Dumpable { } } + /** + * Converts {@link Reason} to {@link PowerManager.WakeReason}. + */ + public static @PowerManager.WakeReason int getPowerManagerWakeReason(@Reason int wakeReason) { + switch (wakeReason) { + case REASON_SENSOR_DOUBLE_TAP: + case REASON_SENSOR_TAP: + return WAKE_REASON_TAP; + case REASON_SENSOR_PICKUP: + return WAKE_REASON_LIFT; + case REASON_SENSOR_UDFPS_LONG_PRESS: + return WAKE_REASON_BIOMETRIC; + case PULSE_REASON_DOCKING: + return WAKE_REASON_PLUGGED_IN; + default: + return WAKE_REASON_GESTURE; + } + } + @Retention(RetentionPolicy.SOURCE) @IntDef({PULSE_REASON_NONE, PULSE_REASON_INTENT, PULSE_REASON_NOTIFICATION, PULSE_REASON_SENSOR_SIGMOTION, REASON_SENSOR_PICKUP, REASON_SENSOR_DOUBLE_TAP, diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java index f8bd1e712b1b..ba38ab0583d4 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java @@ -116,8 +116,8 @@ public class DozeService extends DreamService @Override public void requestWakeUp(@DozeLog.Reason int reason) { - PowerManager pm = getSystemService(PowerManager.class); - pm.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE, + final PowerManager pm = getSystemService(PowerManager.class); + pm.wakeUp(SystemClock.uptimeMillis(), DozeLog.getPowerManagerWakeReason(reason), "com.android.systemui:NODOZE " + DozeLog.reasonToString(reason)); } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt index abe9355d3375..c882f8adceb6 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt @@ -101,13 +101,11 @@ constructor( transitionViewModel.dreamOverlayTranslationY(it.translationYPx) } .collect { px -> - setElementsTranslationYAtPosition( - px, - ComplicationLayoutParams.POSITION_TOP - ) - setElementsTranslationYAtPosition( - px, - ComplicationLayoutParams.POSITION_BOTTOM + ComplicationLayoutParams.iteratePositions( + { position: Int -> + setElementsTranslationYAtPosition(px, position) + }, + POSITION_TOP or POSITION_BOTTOM ) } } @@ -115,15 +113,15 @@ constructor( /* Alpha animations, when moving from DREAMING->LOCKSCREEN state */ launch { transitionViewModel.dreamOverlayAlpha.collect { alpha -> - setElementsAlphaAtPosition( - alpha = alpha, - position = ComplicationLayoutParams.POSITION_TOP, - fadingOut = true, - ) - setElementsAlphaAtPosition( - alpha = alpha, - position = ComplicationLayoutParams.POSITION_BOTTOM, - fadingOut = true, + ComplicationLayoutParams.iteratePositions( + { position: Int -> + setElementsAlphaAtPosition( + alpha = alpha, + position = position, + fadingOut = true, + ) + }, + POSITION_TOP or POSITION_BOTTOM ) } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamCallbackController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayCallbackController.kt index ab4632b08fa1..d5ff8f21abb2 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamCallbackController.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayCallbackController.kt @@ -20,26 +20,39 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.policy.CallbackController import javax.inject.Inject -/** Dream-related callback information */ +/** Dream overlay-related callback information */ @SysUISingleton -class DreamCallbackController @Inject constructor() : - CallbackController<DreamCallbackController.DreamCallback> { +class DreamOverlayCallbackController @Inject constructor() : + CallbackController<DreamOverlayCallbackController.Callback> { - private val callbacks = mutableSetOf<DreamCallbackController.DreamCallback>() + private val callbacks = mutableSetOf<DreamOverlayCallbackController.Callback>() - override fun addCallback(callback: DreamCallbackController.DreamCallback) { + var isDreaming = false + private set + + override fun addCallback(callback: DreamOverlayCallbackController.Callback) { callbacks.add(callback) } - override fun removeCallback(callback: DreamCallbackController.DreamCallback) { + override fun removeCallback(callback: DreamOverlayCallbackController.Callback) { callbacks.remove(callback) } fun onWakeUp() { + isDreaming = false callbacks.forEach { it.onWakeUp() } } - interface DreamCallback { + fun onStartDream() { + isDreaming = true + callbacks.forEach { it.onStartDream() } + } + + interface Callback { + /** Dream overlay has ended */ fun onWakeUp() + + /** Dream overlay has started */ + fun onStartDream() } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index 1be9cd198604..a9a9caed314b 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -64,7 +64,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ // A controller for the dream overlay container view (which contains both the status bar and the // content area). private DreamOverlayContainerViewController mDreamOverlayContainerViewController; - private final DreamCallbackController mDreamCallbackController; + private final DreamOverlayCallbackController mDreamOverlayCallbackController; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; @Nullable private final ComponentName mLowLightDreamComponent; @@ -134,7 +134,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ UiEventLogger uiEventLogger, @Nullable @Named(LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT) ComponentName lowLightDreamComponent, - DreamCallbackController dreamCallbackController) { + DreamOverlayCallbackController dreamOverlayCallbackController) { mContext = context; mExecutor = executor; mWindowManager = windowManager; @@ -143,7 +143,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mKeyguardUpdateMonitor.registerCallback(mKeyguardCallback); mStateController = stateController; mUiEventLogger = uiEventLogger; - mDreamCallbackController = dreamCallbackController; + mDreamOverlayCallbackController = dreamOverlayCallbackController; final ViewModelStore viewModelStore = new ViewModelStore(); final Complication.Host host = @@ -203,6 +203,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ dreamComponent != null && dreamComponent.equals(mLowLightDreamComponent)); mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START); + mDreamOverlayCallbackController.onStartDream(); mStarted = true; }); } @@ -219,7 +220,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ public void onWakeUp(@NonNull Runnable onCompletedCallback) { mExecutor.execute(() -> { if (mDreamOverlayContainerViewController != null) { - mDreamCallbackController.onWakeUp(); + mDreamOverlayCallbackController.onWakeUp(); mDreamOverlayContainerViewController.wakeUp(onCompletedCallback, mExecutor); } }); @@ -229,6 +230,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ * Inserts {@link Window} to host the dream overlay into the dream's parent window. Must be * called from the main executing thread. The window attributes closely mirror those that are * set by the {@link android.service.dreams.DreamService} on the dream Window. + * * @param layoutParams The {@link android.view.WindowManager.LayoutParams} which allow inserting * into the dream window. */ @@ -275,7 +277,11 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ private void resetCurrentDreamOverlayLocked() { if (mStarted && mWindow != null) { - mWindowManager.removeView(mWindow.getDecorView()); + try { + mWindowManager.removeView(mWindow.getDecorView()); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Error removing decor view when resetting overlay", e); + } } mStateController.setOverlayActive(false); diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 1f80b5a99c60..dbeef8d2383b 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -325,6 +325,8 @@ object Flags { // TODO(b/254512758): Tracking Bug @JvmField val ROUNDED_BOX_RIPPLE = releasedFlag(1002, "rounded_box_ripple") + val SHOW_LOWLIGHT_ON_DIRECT_BOOT = unreleasedFlag(1003, "show_lowlight_on_direct_boot") + // 1100 - windowing @Keep @JvmField diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 9a0fbbf60f60..a4fd087a24b1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -29,8 +29,7 @@ import com.android.systemui.doze.DozeHost import com.android.systemui.doze.DozeMachine import com.android.systemui.doze.DozeTransitionCallback import com.android.systemui.doze.DozeTransitionListener -import com.android.systemui.dreams.DreamCallbackController -import com.android.systemui.dreams.DreamCallbackController.DreamCallback +import com.android.systemui.dreams.DreamOverlayCallbackController import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.BiometricUnlockSource @@ -49,7 +48,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.merge /** Defines interface for classes that encapsulate application state for the keyguard. */ interface KeyguardRepository { @@ -81,6 +79,9 @@ interface KeyguardRepository { */ val isKeyguardShowing: Flow<Boolean> + /** Is an activity showing over the keyguard? */ + val isKeyguardOccluded: Flow<Boolean> + /** Observable for the signal that keyguard is about to go away. */ val isKeyguardGoingAway: Flow<Boolean> @@ -107,6 +108,9 @@ interface KeyguardRepository { */ val isDreaming: Flow<Boolean> + /** Observable for whether the device is dreaming with an overlay, see [DreamOverlayService] */ + val isDreamingWithOverlay: Flow<Boolean> + /** * Observable for the amount of doze we are currently in. * @@ -179,7 +183,7 @@ constructor( private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val dozeTransitionListener: DozeTransitionListener, private val authController: AuthController, - private val dreamCallbackController: DreamCallbackController, + private val dreamOverlayCallbackController: DreamOverlayCallbackController, ) : KeyguardRepository { private val _animateBottomAreaDozingTransitions = MutableStateFlow(false) override val animateBottomAreaDozingTransitions = @@ -191,28 +195,55 @@ constructor( private val _clockPosition = MutableStateFlow(Position(0, 0)) override val clockPosition = _clockPosition.asStateFlow() - override val isKeyguardShowing: Flow<Boolean> = conflatedCallbackFlow { - val callback = - object : KeyguardStateController.Callback { - override fun onKeyguardShowingChanged() { - trySendWithFailureLogging( - keyguardStateController.isShowing, - TAG, - "updated isKeyguardShowing" - ) - } + override val isKeyguardShowing: Flow<Boolean> = + conflatedCallbackFlow { + val callback = + object : KeyguardStateController.Callback { + override fun onKeyguardShowingChanged() { + trySendWithFailureLogging( + keyguardStateController.isShowing, + TAG, + "updated isKeyguardShowing" + ) + } + } + + keyguardStateController.addCallback(callback) + // Adding the callback does not send an initial update. + trySendWithFailureLogging( + keyguardStateController.isShowing, + TAG, + "initial isKeyguardShowing" + ) + + awaitClose { keyguardStateController.removeCallback(callback) } } + .distinctUntilChanged() - keyguardStateController.addCallback(callback) - // Adding the callback does not send an initial update. - trySendWithFailureLogging( - keyguardStateController.isShowing, - TAG, - "initial isKeyguardShowing" - ) + override val isKeyguardOccluded: Flow<Boolean> = + conflatedCallbackFlow { + val callback = + object : KeyguardStateController.Callback { + override fun onKeyguardShowingChanged() { + trySendWithFailureLogging( + keyguardStateController.isOccluded, + TAG, + "updated isKeyguardOccluded" + ) + } + } - awaitClose { keyguardStateController.removeCallback(callback) } - } + keyguardStateController.addCallback(callback) + // Adding the callback does not send an initial update. + trySendWithFailureLogging( + keyguardStateController.isOccluded, + TAG, + "initial isKeyguardOccluded" + ) + + awaitClose { keyguardStateController.removeCallback(callback) } + } + .distinctUntilChanged() override val isKeyguardGoingAway: Flow<Boolean> = conflatedCallbackFlow { val callback = @@ -279,36 +310,45 @@ constructor( } .distinctUntilChanged() - override val isDreaming: Flow<Boolean> = - merge( - conflatedCallbackFlow { - val callback = - object : KeyguardUpdateMonitorCallback() { - override fun onDreamingStateChanged(isDreaming: Boolean) { - trySendWithFailureLogging(isDreaming, TAG, "updated isDreaming") - } + override val isDreamingWithOverlay: Flow<Boolean> = + conflatedCallbackFlow { + val callback = + object : DreamOverlayCallbackController.Callback { + override fun onStartDream() { + trySendWithFailureLogging(true, TAG, "updated isDreamingWithOverlay") } - keyguardUpdateMonitor.registerCallback(callback) - trySendWithFailureLogging( - keyguardUpdateMonitor.isDreaming, - TAG, - "initial isDreaming", - ) + override fun onWakeUp() { + trySendWithFailureLogging(false, TAG, "updated isDreamingWithOverlay") + } + } + dreamOverlayCallbackController.addCallback(callback) + trySendWithFailureLogging( + dreamOverlayCallbackController.isDreaming, + TAG, + "initial isDreamingWithOverlay", + ) + + awaitClose { dreamOverlayCallbackController.removeCallback(callback) } + } + .distinctUntilChanged() - awaitClose { keyguardUpdateMonitor.removeCallback(callback) } - }, - conflatedCallbackFlow { - val callback = - object : DreamCallback { - override fun onWakeUp() { - trySendWithFailureLogging(false, TAG, "updated isDreaming") - } + override val isDreaming: Flow<Boolean> = + conflatedCallbackFlow { + val callback = + object : KeyguardUpdateMonitorCallback() { + override fun onDreamingStateChanged(isDreaming: Boolean) { + trySendWithFailureLogging(isDreaming, TAG, "updated isDreaming") } - dreamCallbackController.addCallback(callback) + } + keyguardUpdateMonitor.registerCallback(callback) + trySendWithFailureLogging( + keyguardUpdateMonitor.isDreaming, + TAG, + "initial isDreaming", + ) - awaitClose { dreamCallbackController.removeCallback(callback) } - } - ) + awaitClose { keyguardUpdateMonitor.removeCallback(callback) } + } .distinctUntilChanged() override val linearDozeAmount: Flow<Float> = conflatedCallbackFlow { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt index d72d7183b0f0..343c2dc172fc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -131,6 +131,10 @@ class KeyguardTransitionRepositoryImpl @Inject constructor() : KeyguardTransitio } override fun startTransition(info: TransitionInfo): UUID? { + if (lastStep.from == info.from && lastStep.to == info.to) { + Log.i(TAG, "Duplicate call to start the transition, rejecting: $info") + return null + } if (lastStep.transitionState != TransitionState.FINISHED) { Log.i(TAG, "Transition still active: $lastStep, canceling") } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt index f3d2905121bb..c2d139c21074 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt @@ -21,6 +21,7 @@ import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo @@ -30,33 +31,33 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @SysUISingleton -class AodLockscreenTransitionInteractor +class FromAodTransitionInteractor @Inject constructor( @Application private val scope: CoroutineScope, private val keyguardInteractor: KeyguardInteractor, private val keyguardTransitionRepository: KeyguardTransitionRepository, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, -) : TransitionInteractor(AodLockscreenTransitionInteractor::class.simpleName!!) { +) : TransitionInteractor(FromAodTransitionInteractor::class.simpleName!!) { override fun start() { - listenForTransitionToAodFromLockscreen() - listenForTransitionToLockscreenFromDozeStates() + listenForAodToLockscreen() + listenForAodToGone() } - private fun listenForTransitionToAodFromLockscreen() { + private fun listenForAodToLockscreen() { scope.launch { keyguardInteractor - .dozeTransitionTo(DozeStateModel.DOZE_AOD) + .dozeTransitionTo(DozeStateModel.FINISH) .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) .collect { pair -> val (dozeToAod, lastStartedStep) = pair - if (lastStartedStep.to == KeyguardState.LOCKSCREEN) { + if (lastStartedStep.to == KeyguardState.AOD) { keyguardTransitionRepository.startTransition( TransitionInfo( name, - KeyguardState.LOCKSCREEN, KeyguardState.AOD, + KeyguardState.LOCKSCREEN, getAnimator(), ) ) @@ -65,20 +66,20 @@ constructor( } } - private fun listenForTransitionToLockscreenFromDozeStates() { - val canGoToLockscreen = setOf(KeyguardState.AOD, KeyguardState.DOZING) + private fun listenForAodToGone() { scope.launch { - keyguardInteractor - .dozeTransitionTo(DozeStateModel.FINISH) - .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) + keyguardInteractor.biometricUnlockState + .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair) .collect { pair -> - val (dozeToAod, lastStartedStep) = pair - if (canGoToLockscreen.contains(lastStartedStep.to)) { + val (biometricUnlockState, keyguardState) = pair + if ( + keyguardState == KeyguardState.AOD && isWakeAndUnlock(biometricUnlockState) + ) { keyguardTransitionRepository.startTransition( TransitionInfo( name, - lastStartedStep.to, - KeyguardState.LOCKSCREEN, + KeyguardState.AOD, + KeyguardState.GONE, getAnimator(), ) ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerToGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromBouncerTransitionInteractor.kt index 056c44dc72cf..0e9c44703205 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerToGoneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromBouncerTransitionInteractor.kt @@ -23,16 +23,18 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.util.kotlin.sample import java.util.UUID import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch @SysUISingleton -class BouncerToGoneTransitionInteractor +class FromBouncerTransitionInteractor @Inject constructor( @Application private val scope: CoroutineScope, @@ -40,15 +42,54 @@ constructor( private val shadeRepository: ShadeRepository, private val keyguardTransitionRepository: KeyguardTransitionRepository, private val keyguardTransitionInteractor: KeyguardTransitionInteractor -) : TransitionInteractor(BouncerToGoneTransitionInteractor::class.simpleName!!) { +) : TransitionInteractor(FromBouncerTransitionInteractor::class.simpleName!!) { private var transitionId: UUID? = null override fun start() { - listenForKeyguardGoingAway() + listenForBouncerToGone() + listenForBouncerToLockscreenOrAod() } - private fun listenForKeyguardGoingAway() { + private fun listenForBouncerToLockscreenOrAod() { + scope.launch { + keyguardInteractor.isBouncerShowing + .sample( + combine( + keyguardInteractor.wakefulnessModel, + keyguardTransitionInteractor.startedKeyguardTransitionStep, + ::Pair + ), + ::toTriple + ) + .collect { triple -> + val (isBouncerShowing, wakefulnessState, lastStartedTransitionStep) = triple + if ( + !isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.BOUNCER + ) { + val to = + if ( + wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP || + wakefulnessState.state == WakefulnessState.ASLEEP + ) { + KeyguardState.AOD + } else { + KeyguardState.LOCKSCREEN + } + keyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = name, + from = KeyguardState.BOUNCER, + to = to, + animator = getAnimator(), + ) + ) + } + } + } + } + + private fun listenForBouncerToGone() { scope.launch { keyguardInteractor.isKeyguardGoingAway .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) }) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index 95d96025cf4a..fd2d271e40f9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -21,36 +21,46 @@ import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.util.kotlin.sample import javax.inject.Inject +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch @SysUISingleton -class LockscreenGoneTransitionInteractor +class FromDozingTransitionInteractor @Inject constructor( @Application private val scope: CoroutineScope, private val keyguardInteractor: KeyguardInteractor, - private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val keyguardTransitionRepository: KeyguardTransitionRepository, -) : TransitionInteractor(LockscreenGoneTransitionInteractor::class.simpleName!!) { + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, +) : TransitionInteractor(FromDozingTransitionInteractor::class.simpleName!!) { override fun start() { + listenForDozingToLockscreen() + } + + private fun listenForDozingToLockscreen() { scope.launch { - keyguardInteractor.isKeyguardGoingAway + keyguardInteractor.dozeTransitionModel .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) .collect { pair -> - val (isKeyguardGoingAway, lastStartedStep) = pair - if (isKeyguardGoingAway && lastStartedStep.to == KeyguardState.LOCKSCREEN) { + val (dozeTransitionModel, lastStartedTransition) = pair + if ( + isDozeOff(dozeTransitionModel.to) && + lastStartedTransition.to == KeyguardState.DOZING + ) { keyguardTransitionRepository.startTransition( TransitionInfo( name, + KeyguardState.DOZING, KeyguardState.LOCKSCREEN, - KeyguardState.GONE, getAnimator(), ) ) @@ -59,14 +69,14 @@ constructor( } } - private fun getAnimator(): ValueAnimator { + private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator { return ValueAnimator().apply { setInterpolator(Interpolators.LINEAR) - setDuration(TRANSITION_DURATION_MS) + setDuration(duration.inWholeMilliseconds) } } companion object { - private const val TRANSITION_DURATION_MS = 10L + private val DEFAULT_DURATION = 500.milliseconds } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt index 188930c36097..3b09ae7ba8ea 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt @@ -36,52 +36,48 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch @SysUISingleton -class DreamingTransitionInteractor +class FromDreamingTransitionInteractor @Inject constructor( @Application private val scope: CoroutineScope, private val keyguardInteractor: KeyguardInteractor, private val keyguardTransitionRepository: KeyguardTransitionRepository, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, -) : TransitionInteractor(DreamingTransitionInteractor::class.simpleName!!) { - - private val canDreamFrom = - setOf(KeyguardState.LOCKSCREEN, KeyguardState.GONE, KeyguardState.DOZING) +) : TransitionInteractor(FromDreamingTransitionInteractor::class.simpleName!!) { override fun start() { - listenForEntryToDreaming() listenForDreamingToLockscreen() + listenForDreamingToOccluded() listenForDreamingToGone() listenForDreamingToDozing() } - private fun listenForEntryToDreaming() { + private fun listenForDreamingToLockscreen() { scope.launch { - keyguardInteractor.isDreaming + // Using isDreamingWithOverlay provides an optimized path to LOCKSCREEN state, which + // otherwise would have gone through OCCLUDED first + keyguardInteractor.isDreamingWithOverlay .sample( combine( keyguardInteractor.dozeTransitionModel, - keyguardTransitionInteractor.finishedKeyguardState, + keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair ), ::toTriple ) .collect { triple -> - val (isDreaming, dozeTransitionModel, keyguardState) = triple - // Dozing/AOD and dreaming have overlapping events. If the state remains in - // FINISH, it means that doze mode is not running and DREAMING is ok to - // commence. + val (isDreaming, dozeTransitionModel, lastStartedTransition) = triple if ( - isDozeOff(dozeTransitionModel.to) && - isDreaming && - canDreamFrom.contains(keyguardState) + !isDreaming && + isDozeOff(dozeTransitionModel.to) && + lastStartedTransition.to == KeyguardState.DREAMING ) { keyguardTransitionRepository.startTransition( TransitionInfo( name, - keyguardState, KeyguardState.DREAMING, - getAnimator(), + KeyguardState.LOCKSCREEN, + getAnimator(TO_LOCKSCREEN_DURATION), ) ) } @@ -89,30 +85,35 @@ constructor( } } - private fun listenForDreamingToLockscreen() { + private fun listenForDreamingToOccluded() { scope.launch { keyguardInteractor.isDreaming .sample( combine( - keyguardInteractor.dozeTransitionModel, + keyguardInteractor.isKeyguardOccluded, keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair, ), ::toTriple ) .collect { triple -> - val (isDreaming, dozeTransitionModel, lastStartedTransition) = triple + val (isDreaming, isOccluded, lastStartedTransition) = triple if ( - isDozeOff(dozeTransitionModel.to) && + isOccluded && !isDreaming && - lastStartedTransition.to == KeyguardState.DREAMING + (lastStartedTransition.to == KeyguardState.DREAMING || + lastStartedTransition.to == KeyguardState.LOCKSCREEN) ) { + // At the moment, checking for LOCKSCREEN state above provides a corrective + // action. There's no great signal to determine when the dream is ending + // and a transition to OCCLUDED is beginning directly. For now, the solution + // is DREAMING->LOCKSCREEN->OCCLUDED keyguardTransitionRepository.startTransition( TransitionInfo( name, - KeyguardState.DREAMING, - KeyguardState.LOCKSCREEN, - getAnimator(TO_LOCKSCREEN_DURATION), + lastStartedTransition.to, + KeyguardState.OCCLUDED, + getAnimator(), ) ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GoneAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt index a50e75909dd8..553fafeb92c3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GoneAodTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt @@ -30,19 +30,44 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @SysUISingleton -class GoneAodTransitionInteractor +class FromGoneTransitionInteractor @Inject constructor( @Application private val scope: CoroutineScope, private val keyguardInteractor: KeyguardInteractor, private val keyguardTransitionRepository: KeyguardTransitionRepository, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, -) : TransitionInteractor(GoneAodTransitionInteractor::class.simpleName!!) { +) : TransitionInteractor(FromGoneTransitionInteractor::class.simpleName!!) { override fun start() { + listenForGoneToAod() + listenForGoneToDreaming() + } + + private fun listenForGoneToDreaming() { + scope.launch { + keyguardInteractor.isAbleToDream + .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair) + .collect { pair -> + val (isAbleToDream, keyguardState) = pair + if (isAbleToDream && keyguardState == KeyguardState.GONE) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.GONE, + KeyguardState.DREAMING, + getAnimator(), + ) + ) + } + } + } + } + + private fun listenForGoneToAod() { scope.launch { keyguardInteractor.wakefulnessModel - .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) }) + .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair) .collect { pair -> val (wakefulnessState, keyguardState) = pair if ( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt index 5cb7d709a1a2..326acc9eb762 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt @@ -21,11 +21,11 @@ import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState.KEYGUARD import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.keyguard.shared.model.TransitionState -import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.util.kotlin.sample import java.util.UUID @@ -36,57 +36,54 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch @SysUISingleton -class LockscreenBouncerTransitionInteractor +class FromLockscreenTransitionInteractor @Inject constructor( @Application private val scope: CoroutineScope, private val keyguardInteractor: KeyguardInteractor, private val shadeRepository: ShadeRepository, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val keyguardTransitionRepository: KeyguardTransitionRepository, - private val keyguardTransitionInteractor: KeyguardTransitionInteractor -) : TransitionInteractor(LockscreenBouncerTransitionInteractor::class.simpleName!!) { +) : TransitionInteractor(FromLockscreenTransitionInteractor::class.simpleName!!) { private var transitionId: UUID? = null override fun start() { - listenForDraggingUpToBouncer() - listenForBouncer() + listenForLockscreenToGone() + listenForLockscreenToOccluded() + listenForLockscreenToAod() + listenForLockscreenToBouncer() + listenForLockscreenToDreaming() + listenForLockscreenToBouncerDragging() } - private fun listenForBouncer() { + private fun listenForLockscreenToDreaming() { scope.launch { - keyguardInteractor.isBouncerShowing - .sample( - combine( - keyguardInteractor.wakefulnessModel, - keyguardTransitionInteractor.startedKeyguardTransitionStep, - ::Pair - ), - ::toTriple - ) - .collect { triple -> - val (isBouncerShowing, wakefulnessState, lastStartedTransitionStep) = triple - if ( - !isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.BOUNCER - ) { - val to = - if ( - wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP || - wakefulnessState.state == WakefulnessState.ASLEEP - ) { - KeyguardState.AOD - } else { - KeyguardState.LOCKSCREEN - } + keyguardInteractor.isAbleToDream + .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) + .collect { pair -> + val (isAbleToDream, lastStartedTransition) = pair + if (isAbleToDream && lastStartedTransition.to == KeyguardState.LOCKSCREEN) { keyguardTransitionRepository.startTransition( TransitionInfo( - ownerName = name, - from = KeyguardState.BOUNCER, - to = to, - animator = getAnimator(), + name, + KeyguardState.LOCKSCREEN, + KeyguardState.DREAMING, + getAnimator(), ) ) - } else if ( + } + } + } + } + + private fun listenForLockscreenToBouncer() { + scope.launch { + keyguardInteractor.isBouncerShowing + .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) + .collect { pair -> + val (isBouncerShowing, lastStartedTransitionStep) = pair + if ( isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.LOCKSCREEN ) { keyguardTransitionRepository.startTransition( @@ -98,13 +95,12 @@ constructor( ) ) } - Unit } } } /* Starts transitions when manually dragging up the bouncer from the lockscreen. */ - private fun listenForDraggingUpToBouncer() { + private fun listenForLockscreenToBouncerDragging() { scope.launch { shadeRepository.shadeModel .sample( @@ -157,6 +153,76 @@ constructor( } } + private fun listenForLockscreenToGone() { + scope.launch { + keyguardInteractor.isKeyguardGoingAway + .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) + .collect { pair -> + val (isKeyguardGoingAway, lastStartedStep) = pair + if (isKeyguardGoingAway && lastStartedStep.to == KeyguardState.LOCKSCREEN) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.LOCKSCREEN, + KeyguardState.GONE, + getAnimator(), + ) + ) + } + } + } + } + + private fun listenForLockscreenToOccluded() { + scope.launch { + keyguardInteractor.isKeyguardOccluded + .sample( + combine( + keyguardTransitionInteractor.finishedKeyguardState, + keyguardInteractor.isDreaming, + ::Pair + ), + ::toTriple + ) + .collect { triple -> + val (isOccluded, keyguardState, isDreaming) = triple + // Occlusion signals come from the framework, and should interrupt any + // existing transition + if (isOccluded && !isDreaming) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + keyguardState, + KeyguardState.OCCLUDED, + getAnimator(), + ) + ) + } + } + } + } + + private fun listenForLockscreenToAod() { + scope.launch { + keyguardInteractor + .dozeTransitionTo(DozeStateModel.DOZE_AOD) + .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) + .collect { pair -> + val (dozeToAod, lastStartedStep) = pair + if (lastStartedStep.to == KeyguardState.LOCKSCREEN) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.LOCKSCREEN, + KeyguardState.AOD, + getAnimator(), + ) + ) + } + } + } + } + private fun getAnimator(): ValueAnimator { return ValueAnimator().apply { setInterpolator(Interpolators.LINEAR) @@ -165,6 +231,6 @@ constructor( } companion object { - private const val TRANSITION_DURATION_MS = 300L + private const val TRANSITION_DURATION_MS = 500L } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodToGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt index dad166f2b5e0..88789019b10f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodToGoneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt @@ -21,39 +21,43 @@ import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository -import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.util.kotlin.sample import javax.inject.Inject +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch @SysUISingleton -class AodToGoneTransitionInteractor +class FromOccludedTransitionInteractor @Inject constructor( @Application private val scope: CoroutineScope, private val keyguardInteractor: KeyguardInteractor, private val keyguardTransitionRepository: KeyguardTransitionRepository, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, -) : TransitionInteractor(AodToGoneTransitionInteractor::class.simpleName!!) { +) : TransitionInteractor(FromOccludedTransitionInteractor::class.simpleName!!) { override fun start() { + listenForOccludedToLockscreen() + listenForOccludedToDreaming() + } + + private fun listenForOccludedToDreaming() { scope.launch { - keyguardInteractor.biometricUnlockState - .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) }) + keyguardInteractor.isAbleToDream + .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair) .collect { pair -> - val (biometricUnlockState, keyguardState) = pair - if ( - keyguardState == KeyguardState.AOD && isWakeAndUnlock(biometricUnlockState) - ) { + val (isAbleToDream, keyguardState) = pair + if (isAbleToDream && keyguardState == KeyguardState.OCCLUDED) { keyguardTransitionRepository.startTransition( TransitionInfo( name, - KeyguardState.AOD, - KeyguardState.GONE, + KeyguardState.OCCLUDED, + KeyguardState.DREAMING, getAnimator(), ) ) @@ -62,14 +66,37 @@ constructor( } } - private fun getAnimator(): ValueAnimator { + private fun listenForOccludedToLockscreen() { + scope.launch { + keyguardInteractor.isKeyguardOccluded + .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) + .collect { pair -> + val (isOccluded, lastStartedKeyguardState) = pair + // Occlusion signals come from the framework, and should interrupt any + // existing transition + if (!isOccluded && lastStartedKeyguardState.to == KeyguardState.OCCLUDED) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.OCCLUDED, + KeyguardState.LOCKSCREEN, + getAnimator(TO_LOCKSCREEN_DURATION), + ) + ) + } + } + } + } + + private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator { return ValueAnimator().apply { setInterpolator(Interpolators.LINEAR) - setDuration(TRANSITION_DURATION_MS) + setDuration(duration.inWholeMilliseconds) } } companion object { - private const val TRANSITION_DURATION_MS = 500L + private val DEFAULT_DURATION = 500.milliseconds + val TO_LOCKSCREEN_DURATION = 933.milliseconds } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 6912e1d3558e..402c1793f0b2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -22,12 +22,16 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.DozeStateModel +import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff import com.android.systemui.keyguard.shared.model.DozeTransitionModel import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.keyguard.shared.model.WakefulnessModel +import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.merge /** * Encapsulates business-logic related to the keyguard but not to a more specific part within it. @@ -52,8 +56,27 @@ constructor( * but not vice-versa. */ val isDreaming: Flow<Boolean> = repository.isDreaming + /** Whether the system is dreaming with an overlay active */ + val isDreamingWithOverlay: Flow<Boolean> = repository.isDreamingWithOverlay + + /** + * Dozing and dreaming have overlapping events. If the doze state remains in FINISH, it means + * that doze mode is not running and DREAMING is ok to commence. + */ + val isAbleToDream: Flow<Boolean> = + merge(isDreaming, isDreamingWithOverlay) + .sample( + dozeTransitionModel, + { isDreaming, dozeTransitionModel -> + isDreaming && isDozeOff(dozeTransitionModel.to) + } + ) + .distinctUntilChanged() + /** Whether the keyguard is showing or not. */ val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing + /** Whether the keyguard is occluded (covered by an activity). */ + val isKeyguardOccluded: Flow<Boolean> = repository.isKeyguardOccluded /** Whether the keyguard is going away. */ val isKeyguardGoingAway: Flow<Boolean> = repository.isKeyguardGoingAway /** Whether the bouncer is showing or not. */ @@ -74,8 +97,8 @@ constructor( /** The approximate location on the screen of the face unlock sensor, if one is available. */ val faceSensorLocation: Flow<Point?> = repository.faceSensorLocation - fun dozeTransitionTo(state: DozeStateModel): Flow<DozeTransitionModel> { - return dozeTransitionModel.filter { it.to == state } + fun dozeTransitionTo(vararg states: DozeStateModel): Flow<DozeTransitionModel> { + return dozeTransitionModel.filter { states.contains(it.to) } } fun isKeyguardShowing(): Boolean { return repository.isKeyguardShowing() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt index bb8b79a3aa6b..fbed446b7d9a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt @@ -37,13 +37,13 @@ constructor( // exhaustive val ret = when (it) { - is LockscreenBouncerTransitionInteractor -> Log.d(TAG, "Started $it") - is AodLockscreenTransitionInteractor -> Log.d(TAG, "Started $it") - is GoneAodTransitionInteractor -> Log.d(TAG, "Started $it") - is LockscreenGoneTransitionInteractor -> Log.d(TAG, "Started $it") - is AodToGoneTransitionInteractor -> Log.d(TAG, "Started $it") - is BouncerToGoneTransitionInteractor -> Log.d(TAG, "Started $it") - is DreamingTransitionInteractor -> Log.d(TAG, "Started $it") + is FromBouncerTransitionInteractor -> Log.d(TAG, "Started $it") + is FromAodTransitionInteractor -> Log.d(TAG, "Started $it") + is FromGoneTransitionInteractor -> Log.d(TAG, "Started $it") + is FromLockscreenTransitionInteractor -> Log.d(TAG, "Started $it") + is FromDreamingTransitionInteractor -> Log.d(TAG, "Started $it") + is FromOccludedTransitionInteractor -> Log.d(TAG, "Started $it") + is FromDozingTransitionInteractor -> Log.d(TAG, "Started $it") } it.start() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index 3b9d6f59a8e7..04024be571c8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -24,7 +24,9 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionState.STARTED import com.android.systemui.keyguard.shared.model.TransitionStep import javax.inject.Inject import kotlin.time.Duration @@ -50,6 +52,10 @@ constructor( val dreamingToLockscreenTransition: Flow<TransitionStep> = repository.transition(DREAMING, LOCKSCREEN) + /** OCCLUDED->LOCKSCREEN transition information. */ + val occludedToLockscreenTransition: Flow<TransitionStep> = + repository.transition(OCCLUDED, LOCKSCREEN) + /** (any)->AOD transition information */ val anyStateToAodTransition: Flow<TransitionStep> = repository.transitions.filter { step -> step.to == KeyguardState.AOD } @@ -93,7 +99,14 @@ constructor( val start = (params.startTime / totalDuration).toFloat() val chunks = (totalDuration / params.duration).toFloat() return flow - .map { step -> (step.value - start) * chunks } + // When starting, emit a value of 0f to give animations a chance to set initial state + .map { step -> + if (step.transitionState == STARTED) { + 0f + } else { + (step.value - start) * chunks + } + } .filter { value -> value >= 0f && value <= 1f } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt index 6e25200bc2f6..a59c407182e2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt @@ -89,6 +89,7 @@ constructor( KeyguardState.BOUNCER -> true KeyguardState.LOCKSCREEN -> true KeyguardState.GONE -> true + KeyguardState.OCCLUDED -> true } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt index 5f63ae765854..81fa2336d40d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt @@ -32,25 +32,25 @@ abstract class StartKeyguardTransitionModule { @Binds @IntoSet - abstract fun lockscreenBouncer( - impl: LockscreenBouncerTransitionInteractor - ): TransitionInteractor + abstract fun fromBouncer(impl: FromBouncerTransitionInteractor): TransitionInteractor @Binds @IntoSet - abstract fun aodLockscreen(impl: AodLockscreenTransitionInteractor): TransitionInteractor + abstract fun fromLockscreen(impl: FromLockscreenTransitionInteractor): TransitionInteractor - @Binds @IntoSet abstract fun goneAod(impl: GoneAodTransitionInteractor): TransitionInteractor + @Binds @IntoSet abstract fun fromAod(impl: FromAodTransitionInteractor): TransitionInteractor - @Binds @IntoSet abstract fun aodGone(impl: AodToGoneTransitionInteractor): TransitionInteractor + @Binds @IntoSet abstract fun fromGone(impl: FromGoneTransitionInteractor): TransitionInteractor @Binds @IntoSet - abstract fun bouncerGone(impl: BouncerToGoneTransitionInteractor): TransitionInteractor + abstract fun fromDreaming(impl: FromDreamingTransitionInteractor): TransitionInteractor @Binds @IntoSet - abstract fun lockscreenGone(impl: LockscreenGoneTransitionInteractor): TransitionInteractor + abstract fun fromOccluded(impl: FromOccludedTransitionInteractor): TransitionInteractor - @Binds @IntoSet abstract fun dreaming(impl: DreamingTransitionInteractor): TransitionInteractor + @Binds + @IntoSet + abstract fun fromDozing(impl: FromDozingTransitionInteractor): TransitionInteractor } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt index 08ad3d5bdbf6..4d24c14501aa 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt @@ -31,4 +31,6 @@ sealed class TransitionInteractor(val name: String) { abstract fun start() fun <A, B, C> toTriple(a: A, bc: Pair<B, C>) = Triple(a, bc.first, bc.second) + + fun <A, B, C> toTriple(ab: Pair<A, B>, c: C) = Triple(ab.first, ab.second, c) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt index dd908c420fcb..c7579862a717 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt @@ -57,4 +57,8 @@ enum class KeyguardState { * with SWIPE security method or face unlock without bypass. */ GONE, + /* + * An activity is displaying over the keyguard. + */ + OCCLUDED, } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt index ae8edfece4cb..b19795c38579 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.ui.binder import android.annotation.SuppressLint import android.graphics.drawable.Animatable2 +import android.os.VibrationEffect import android.util.Size import android.util.TypedValue import android.view.MotionEvent @@ -43,8 +44,11 @@ import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.VibratorHelper +import com.android.systemui.util.kotlin.pairwise import kotlin.math.pow import kotlin.math.sqrt +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine @@ -93,6 +97,7 @@ object KeyguardBottomAreaViewBinder { view: ViewGroup, viewModel: KeyguardBottomAreaViewModel, falsingManager: FalsingManager?, + vibratorHelper: VibratorHelper?, messageDisplayer: (Int) -> Unit, ): Binding { val indicationArea: View = view.requireViewById(R.id.keyguard_indication_area) @@ -118,22 +123,48 @@ object KeyguardBottomAreaViewBinder { viewModel = buttonModel, falsingManager = falsingManager, messageDisplayer = messageDisplayer, + vibratorHelper = vibratorHelper, ) } } launch { + viewModel.startButton + .map { it.isActivated } + .pairwise() + .collect { (prev, next) -> + when { + !prev && next -> vibratorHelper?.vibrate(Vibrations.Activated) + prev && !next -> vibratorHelper?.vibrate(Vibrations.Deactivated) + } + } + } + + launch { viewModel.endButton.collect { buttonModel -> updateButton( view = endButton, viewModel = buttonModel, falsingManager = falsingManager, messageDisplayer = messageDisplayer, + vibratorHelper = vibratorHelper, ) } } launch { + viewModel.endButton + .map { it.isActivated } + .pairwise() + .collect { (prev, next) -> + when { + !prev && next -> vibratorHelper?.vibrate(Vibrations.Activated) + prev && !next -> vibratorHelper?.vibrate(Vibrations.Deactivated) + } + } + } + + launch { viewModel.isOverlayContainerVisible.collect { isVisible -> overlayContainer.visibility = if (isVisible) { @@ -239,6 +270,7 @@ object KeyguardBottomAreaViewBinder { viewModel: KeyguardQuickAffordanceViewModel, falsingManager: FalsingManager?, messageDisplayer: (Int) -> Unit, + vibratorHelper: VibratorHelper?, ) { if (!viewModel.isVisible) { view.isVisible = false @@ -312,7 +344,9 @@ object KeyguardBottomAreaViewBinder { view.isClickable = viewModel.isClickable if (viewModel.isClickable) { if (viewModel.useLongPress) { - view.setOnTouchListener(OnTouchListener(view, viewModel, messageDisplayer)) + view.setOnTouchListener( + OnTouchListener(view, viewModel, messageDisplayer, vibratorHelper) + ) } else { view.setOnClickListener(OnClickListener(viewModel, checkNotNull(falsingManager))) } @@ -328,6 +362,7 @@ object KeyguardBottomAreaViewBinder { private val view: View, private val viewModel: KeyguardQuickAffordanceViewModel, private val messageDisplayer: (Int) -> Unit, + private val vibratorHelper: VibratorHelper?, ) : View.OnTouchListener { private val longPressDurationMs = ViewConfiguration.getLongPressTimeout().toLong() @@ -376,25 +411,38 @@ object KeyguardBottomAreaViewBinder { true } MotionEvent.ACTION_UP -> { - if (System.currentTimeMillis() - downTimestamp < longPressDurationMs) { - messageDisplayer.invoke(R.string.keyguard_affordance_press_too_short) - val shakeAnimator = - ObjectAnimator.ofFloat( - view, - "translationX", - 0f, - view.context.resources - .getDimensionPixelSize( - R.dimen.keyguard_affordance_shake_amplitude + cancel( + onAnimationEnd = + if (System.currentTimeMillis() - downTimestamp < longPressDurationMs) { + Runnable { + messageDisplayer.invoke( + R.string.keyguard_affordance_press_too_short ) - .toFloat(), - 0f, - ) - shakeAnimator.duration = 300 - shakeAnimator.interpolator = CycleInterpolator(5f) - shakeAnimator.start() - } - cancel() + val amplitude = + view.context.resources + .getDimensionPixelSize( + R.dimen.keyguard_affordance_shake_amplitude + ) + .toFloat() + val shakeAnimator = + ObjectAnimator.ofFloat( + view, + "translationX", + -amplitude / 2, + amplitude / 2, + ) + shakeAnimator.duration = + ShakeAnimationDuration.inWholeMilliseconds + shakeAnimator.interpolator = + CycleInterpolator(ShakeAnimationCycles) + shakeAnimator.start() + + vibratorHelper?.vibrate(Vibrations.Shake) + } + } else { + null + } + ) true } MotionEvent.ACTION_CANCEL -> { @@ -405,11 +453,11 @@ object KeyguardBottomAreaViewBinder { } } - private fun cancel() { + private fun cancel(onAnimationEnd: Runnable? = null) { downTimestamp = 0L longPressAnimator?.cancel() longPressAnimator = null - view.animate().scaleX(1f).scaleY(1f) + view.animate().scaleX(1f).scaleY(1f).withEndAction(onAnimationEnd) } companion object { @@ -461,4 +509,58 @@ object KeyguardBottomAreaViewBinder { val indicationTextSizePx: Int, val buttonSizePx: Size, ) + + private val ShakeAnimationDuration = 300.milliseconds + private val ShakeAnimationCycles = 5f + + object Vibrations { + + private const val SmallVibrationScale = 0.3f + private const val BigVibrationScale = 0.6f + + val Shake = + VibrationEffect.startComposition() + .apply { + val vibrationDelayMs = + (ShakeAnimationDuration.inWholeMilliseconds / (ShakeAnimationCycles * 2)) + .toInt() + val vibrationCount = ShakeAnimationCycles.toInt() * 2 + repeat(vibrationCount) { + addPrimitive( + VibrationEffect.Composition.PRIMITIVE_TICK, + SmallVibrationScale, + vibrationDelayMs, + ) + } + } + .compose() + + val Activated = + VibrationEffect.startComposition() + .addPrimitive( + VibrationEffect.Composition.PRIMITIVE_TICK, + BigVibrationScale, + 0, + ) + .addPrimitive( + VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, + 0.1f, + 0, + ) + .compose() + + val Deactivated = + VibrationEffect.startComposition() + .addPrimitive( + VibrationEffect.Composition.PRIMITIVE_TICK, + BigVibrationScale, + 0, + ) + .addPrimitive( + VibrationEffect.Composition.PRIMITIVE_QUICK_FALL, + 0.1f, + 0, + ) + .compose() + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt index 402fac1c8fb4..e164f5d58b07 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt @@ -19,7 +19,7 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.domain.interactor.DreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION +import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.AnimationParams import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt new file mode 100644 index 000000000000..e804562bc85f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.AnimationParams +import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +/** + * Breaks down OCCLUDED->LOCKSCREEN transition into discrete steps for corresponding views to + * consume. + */ +@SysUISingleton +class OccludedToLockscreenTransitionViewModel +@Inject +constructor( + private val interactor: KeyguardTransitionInteractor, +) { + /** Lockscreen views y-translation */ + fun lockscreenTranslationY(translatePx: Int): Flow<Float> { + return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value -> + -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx) + } + } + + /** Lockscreen views alpha */ + val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA) + + private fun flowForAnimation(params: AnimationParams): Flow<Float> { + return interactor.transitionStepAnimation( + interactor.occludedToLockscreenTransition, + params, + totalDuration = TO_LOCKSCREEN_DURATION + ) + } + + companion object { + @JvmField val LOCKSCREEN_ANIMATION_DURATION_MS = TO_LOCKSCREEN_DURATION.inWholeMilliseconds + val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_LOCKSCREEN_DURATION) + val LOCKSCREEN_ALPHA = + AnimationParams(startTime = 233.milliseconds, duration = 250.milliseconds) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index a6447a5bf500..5716a1d7260c 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -448,6 +448,10 @@ public class ScreenshotController { // Any cleanup needed when the service is being destroyed. void onDestroy() { + if (mSaveInBgTask != null) { + // just log success/failure for the pre-existing screenshot + mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady); + } removeWindow(); releaseMediaPlayer(); releaseContext(); diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 8054f27029ae..8f512d0205b8 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -144,6 +144,7 @@ import com.android.systemui.keyguard.shared.model.TransitionState; import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel; +import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel; import com.android.systemui.media.controls.pipeline.MediaDataManager; import com.android.systemui.media.controls.ui.KeyguardMediaController; import com.android.systemui.media.controls.ui.MediaHierarchyManager; @@ -238,6 +239,8 @@ import java.util.function.Consumer; import javax.inject.Inject; import javax.inject.Provider; +import kotlinx.coroutines.CoroutineDispatcher; + @CentralSurfacesComponent.CentralSurfacesScope public final class NotificationPanelViewController implements Dumpable { @@ -683,10 +686,13 @@ public final class NotificationPanelViewController implements Dumpable { private boolean mIgnoreXTouchSlop; private boolean mExpandLatencyTracking; private DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel; + private OccludedToLockscreenTransitionViewModel mOccludedToLockscreenTransitionViewModel; private KeyguardTransitionInteractor mKeyguardTransitionInteractor; - private boolean mIsDreamToLockscreenTransitionRunning = false; + private CoroutineDispatcher mMainDispatcher; + private boolean mIsToLockscreenTransitionRunning = false; private int mDreamingToLockscreenTransitionTranslationY; + private int mOccludedToLockscreenTransitionTranslationY; private boolean mUnocclusionTransitionFlagEnabled = false; private final Runnable mFlingCollapseRunnable = () -> fling(0, false /* expand */, @@ -705,7 +711,13 @@ public final class NotificationPanelViewController implements Dumpable { private final Consumer<TransitionStep> mDreamingToLockscreenTransition = (TransitionStep step) -> { - mIsDreamToLockscreenTransitionRunning = + mIsToLockscreenTransitionRunning = + step.getTransitionState() == TransitionState.RUNNING; + }; + + private final Consumer<TransitionStep> mOccludedToLockscreenTransition = + (TransitionStep step) -> { + mIsToLockscreenTransitionRunning = step.getTransitionState() == TransitionState.RUNNING; }; @@ -778,6 +790,8 @@ public final class NotificationPanelViewController implements Dumpable { KeyguardBottomAreaViewModel keyguardBottomAreaViewModel, KeyguardBottomAreaInteractor keyguardBottomAreaInteractor, DreamingToLockscreenTransitionViewModel dreamingToLockscreenTransitionViewModel, + OccludedToLockscreenTransitionViewModel occludedToLockscreenTransitionViewModel, + @Main CoroutineDispatcher mainDispatcher, KeyguardTransitionInteractor keyguardTransitionInteractor, DumpManager dumpManager) { keyguardStateController.addCallback(new KeyguardStateController.Callback() { @@ -795,6 +809,7 @@ public final class NotificationPanelViewController implements Dumpable { mShadeHeightLogger = shadeHeightLogger; mGutsManager = gutsManager; mDreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel; + mOccludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel; mKeyguardTransitionInteractor = keyguardTransitionInteractor; mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override @@ -873,6 +888,7 @@ public final class NotificationPanelViewController implements Dumpable { mFalsingCollector = falsingCollector; mPowerManager = powerManager; mWakeUpCoordinator = coordinator; + mMainDispatcher = mainDispatcher; mAccessibilityManager = accessibilityManager; mView.setAccessibilityPaneTitle(determineAccessibilityPaneTitle()); setPanelAlpha(255, false /* animate */); @@ -1097,15 +1113,27 @@ public final class NotificationPanelViewController implements Dumpable { controller.setup(mNotificationContainerParent)); if (mUnocclusionTransitionFlagEnabled) { + // Dreaming->Lockscreen + collectFlow(mView, mKeyguardTransitionInteractor.getDreamingToLockscreenTransition(), + mDreamingToLockscreenTransition, mMainDispatcher); collectFlow(mView, mDreamingToLockscreenTransitionViewModel.getLockscreenAlpha(), - dreamingToLockscreenTransitionAlpha(mNotificationStackScrollLayoutController)); - + toLockscreenTransitionAlpha(mNotificationStackScrollLayoutController), + mMainDispatcher); collectFlow(mView, mDreamingToLockscreenTransitionViewModel.lockscreenTranslationY( mDreamingToLockscreenTransitionTranslationY), - dreamingToLockscreenTransitionY(mNotificationStackScrollLayoutController)); + toLockscreenTransitionY(mNotificationStackScrollLayoutController), + mMainDispatcher); - collectFlow(mView, mKeyguardTransitionInteractor.getDreamingToLockscreenTransition(), - mDreamingToLockscreenTransition); + // Occluded->Lockscreen + collectFlow(mView, mKeyguardTransitionInteractor.getOccludedToLockscreenTransition(), + mOccludedToLockscreenTransition, mMainDispatcher); + collectFlow(mView, mOccludedToLockscreenTransitionViewModel.getLockscreenAlpha(), + toLockscreenTransitionAlpha(mNotificationStackScrollLayoutController), + mMainDispatcher); + collectFlow(mView, mOccludedToLockscreenTransitionViewModel.lockscreenTranslationY( + mOccludedToLockscreenTransitionTranslationY), + toLockscreenTransitionY(mNotificationStackScrollLayoutController), + mMainDispatcher); } } @@ -1143,6 +1171,8 @@ public final class NotificationPanelViewController implements Dumpable { R.dimen.split_shade_scrim_transition_distance); mDreamingToLockscreenTransitionTranslationY = mResources.getDimensionPixelSize( R.dimen.dreaming_to_lockscreen_transition_lockscreen_translation_y); + mOccludedToLockscreenTransitionTranslationY = mResources.getDimensionPixelSize( + R.dimen.occluded_to_lockscreen_transition_lockscreen_translation_y); } private void updateViewControllers(KeyguardStatusView keyguardStatusView, @@ -1370,8 +1400,8 @@ public final class NotificationPanelViewController implements Dumpable { mFalsingManager, mLockIconViewController, stringResourceId -> - mKeyguardIndicationController.showTransientIndication(stringResourceId) - ); + mKeyguardIndicationController.showTransientIndication(stringResourceId), + mVibratorHelper); } @VisibleForTesting @@ -1806,7 +1836,7 @@ public final class NotificationPanelViewController implements Dumpable { } private void updateClock() { - if (mIsDreamToLockscreenTransitionRunning) { + if (mIsToLockscreenTransitionRunning) { return; } float alpha = mClockPositionResult.clockAlpha * mKeyguardOnlyContentAlpha; @@ -2697,7 +2727,7 @@ public final class NotificationPanelViewController implements Dumpable { } else if (statusBarState == KEYGUARD || statusBarState == StatusBarState.SHADE_LOCKED) { mKeyguardBottomArea.setVisibility(View.VISIBLE); - if (!mIsDreamToLockscreenTransitionRunning) { + if (!mIsToLockscreenTransitionRunning) { mKeyguardBottomArea.setAlpha(1f); } } else { @@ -3566,7 +3596,7 @@ public final class NotificationPanelViewController implements Dumpable { } private void updateNotificationTranslucency() { - if (mIsDreamToLockscreenTransitionRunning) { + if (mIsToLockscreenTransitionRunning) { return; } float alpha = 1f; @@ -3624,7 +3654,7 @@ public final class NotificationPanelViewController implements Dumpable { } private void updateKeyguardBottomAreaAlpha() { - if (mIsDreamToLockscreenTransitionRunning) { + if (mIsToLockscreenTransitionRunning) { return; } // There are two possible panel expansion behaviors: @@ -5856,7 +5886,7 @@ public final class NotificationPanelViewController implements Dumpable { mCurrentPanelState = state; } - private Consumer<Float> dreamingToLockscreenTransitionAlpha( + private Consumer<Float> toLockscreenTransitionAlpha( NotificationStackScrollLayoutController stackScroller) { return (Float alpha) -> { mKeyguardStatusViewController.setAlpha(alpha); @@ -5874,7 +5904,7 @@ public final class NotificationPanelViewController implements Dumpable { }; } - private Consumer<Float> dreamingToLockscreenTransitionY( + private Consumer<Float> toLockscreenTransitionY( NotificationStackScrollLayoutController stackScroller) { return (Float translationY) -> { mKeyguardStatusViewController.setTranslationY(translationY, /* excludeMedia= */false); diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index d773c0103c93..5c1ddd601f96 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -19,6 +19,7 @@ package com.android.systemui.shade; import android.app.StatusBarManager; import android.media.AudioManager; import android.media.session.MediaSessionLegacyHelper; +import android.os.PowerManager; import android.os.SystemClock; import android.util.Log; import android.view.GestureDetector; @@ -238,7 +239,9 @@ public class NotificationShadeWindowViewController { () -> mService.wakeUpIfDozing( SystemClock.uptimeMillis(), mView, - "LOCK_ICON_TOUCH")); + "LOCK_ICON_TOUCH", + PowerManager.WAKE_REASON_GESTURE) + ); // In case we start outside of the view bounds (below the status bar), we need to // dispatch diff --git a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt index bf622c941abb..db700650e46c 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt @@ -17,6 +17,7 @@ package com.android.systemui.shade import android.hardware.display.AmbientDisplayConfiguration +import android.os.PowerManager import android.os.SystemClock import android.os.UserHandle import android.provider.Settings @@ -89,7 +90,8 @@ class PulsingGestureListener @Inject constructor( centralSurfaces.wakeUpIfDozing( SystemClock.uptimeMillis(), notificationShadeWindowView, - "PULSING_SINGLE_TAP" + "PULSING_SINGLE_TAP", + PowerManager.WAKE_REASON_TAP ) } return true @@ -114,7 +116,9 @@ class PulsingGestureListener @Inject constructor( centralSurfaces.wakeUpIfDozing( SystemClock.uptimeMillis(), notificationShadeWindowView, - "PULSING_DOUBLE_TAP") + "PULSING_DOUBLE_TAP", + PowerManager.WAKE_REASON_TAP + ) return true } return false diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt index b8302d706e8d..905cc3fc71e0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt @@ -5,6 +5,7 @@ import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator import android.content.Context import android.content.res.Configuration +import android.os.PowerManager import android.os.SystemClock import android.util.IndentingPrintWriter import android.util.MathUtils @@ -272,7 +273,12 @@ class LockscreenShadeTransitionController @Inject constructor( // Bind the click listener of the shelf to go to the full shade notificationShelfController.setOnClickListener { if (statusBarStateController.state == StatusBarState.KEYGUARD) { - centralSurfaces.wakeUpIfDozing(SystemClock.uptimeMillis(), it, "SHADE_CLICK") + centralSurfaces.wakeUpIfDozing( + SystemClock.uptimeMillis(), + it, + "SHADE_CLICK", + PowerManager.WAKE_REASON_GESTURE, + ) goToLockedShade(it) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java index 80a891319f6c..8f9365cd4dc4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java @@ -24,6 +24,7 @@ import android.app.RemoteInput; import android.content.Context; import android.content.Intent; import android.content.pm.UserInfo; +import android.os.PowerManager; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; @@ -64,6 +65,8 @@ import com.android.systemui.statusbar.policy.RemoteInputView; import com.android.systemui.util.DumpUtilsKt; import com.android.systemui.util.ListenerSet; +import dagger.Lazy; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; @@ -71,8 +74,6 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; -import dagger.Lazy; - /** * Class for handling remote input state over a set of notifications. This class handles things * like keeping notifications temporarily that were cancelled as a response to a remote input @@ -120,7 +121,8 @@ public class NotificationRemoteInputManager implements Dumpable { View view, PendingIntent pendingIntent, RemoteViews.RemoteResponse response) { mCentralSurfacesOptionalLazy.get().ifPresent( centralSurfaces -> centralSurfaces.wakeUpIfDozing( - SystemClock.uptimeMillis(), view, "NOTIFICATION_CLICK")); + SystemClock.uptimeMillis(), view, "NOTIFICATION_CLICK", + PowerManager.WAKE_REASON_GESTURE)); final NotificationEntry entry = getNotificationForParent(view.getParent()); mLogger.logInitialClick(entry, pendingIntent); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt index c630feba1dcb..976924a2159d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt @@ -22,7 +22,6 @@ import android.animation.ValueAnimator import android.content.Context import android.content.res.Configuration import android.os.PowerManager -import android.os.PowerManager.WAKE_REASON_GESTURE import android.os.SystemClock import android.util.IndentingPrintWriter import android.view.MotionEvent @@ -249,7 +248,7 @@ constructor( } if (statusBarStateController.isDozing) { wakeUpCoordinator.willWakeUp = true - mPowerManager!!.wakeUp(SystemClock.uptimeMillis(), WAKE_REASON_GESTURE, + mPowerManager!!.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE, "com.android.systemui:PULSEDRAG") } lockscreenShadeTransitionController.goToLockedShade(startingChild, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java index 57add75ee38a..5f5418f5eeb9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java @@ -1302,7 +1302,7 @@ public class NetworkControllerImpl extends BroadcastReceiver } } String wifi = args.getString("wifi"); - if (wifi != null) { + if (wifi != null && !mStatusBarPipelineFlags.runNewWifiIconBackend()) { boolean show = wifi.equals("show"); String level = args.getString("level"); if (level != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java index c3ce593b54fd..705cf92ee869 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification; import android.app.Notification; +import android.os.PowerManager; import android.os.SystemClock; import android.service.notification.StatusBarNotification; import android.util.Log; @@ -70,7 +71,8 @@ public final class NotificationClicker implements View.OnClickListener { } mCentralSurfacesOptional.ifPresent(centralSurfaces -> centralSurfaces.wakeUpIfDozing( - SystemClock.uptimeMillis(), v, "NOTIFICATION_CLICK")); + SystemClock.uptimeMillis(), v, "NOTIFICATION_CLICK", + PowerManager.WAKE_REASON_GESTURE)); final ExpandableNotificationRow row = (ExpandableNotificationRow) v; final NotificationEntry entry = row.getEntry(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index 5e98f5419e84..895a2934ec1b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -429,7 +429,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp Runnable wakeUp = ()-> { if (!wasDeviceInteractive || mUpdateMonitor.isDreaming()) { mLogger.i("bio wakelock: Authenticated, waking up..."); - mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE, + mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_BIOMETRIC, "android.policy:BIOMETRIC"); } Trace.beginSection("release wake-and-unlock"); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java index 8d06fad0f418..4d0ad405835a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java @@ -25,6 +25,7 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; +import android.os.PowerManager; import android.os.UserHandle; import android.service.notification.StatusBarNotification; import android.view.KeyEvent; @@ -203,7 +204,10 @@ public interface CentralSurfaces extends Dumpable, ActivityStarter, LifecycleOwn @Override Lifecycle getLifecycle(); - void wakeUpIfDozing(long time, View where, String why); + /** + * Wakes up the device if the device was dozing. + */ + void wakeUpIfDozing(long time, View where, String why, @PowerManager.WakeReason int wakeReason); NotificationShadeWindowView getNotificationShadeWindowView(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 1fcfe4ef2f88..198572a51760 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -899,8 +899,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mKeyguardIndicationController.init(); mColorExtractor.addOnColorsChangedListener(mOnColorsChangedListener); - mStatusBarStateController.addCallback(mStateListener, - SysuiStatusBarStateController.RANK_STATUS_BAR); mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); @@ -1519,10 +1517,11 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { * @param why the reason for the wake up */ @Override - public void wakeUpIfDozing(long time, View where, String why) { + public void wakeUpIfDozing(long time, View where, String why, + @PowerManager.WakeReason int wakeReason) { if (mDozing && mScreenOffAnimationController.allowWakeUpIfDozing()) { mPowerManager.wakeUp( - time, PowerManager.WAKE_REASON_GESTURE, "com.android.systemui:" + why); + time, wakeReason, "com.android.systemui:" + why); mWakeUpComingFromTouch = true; mFalsingCollector.onScreenOnFromTouch(); } @@ -1599,6 +1598,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { protected void startKeyguard() { Trace.beginSection("CentralSurfaces#startKeyguard"); + mStatusBarStateController.addCallback(mStateListener, + SysuiStatusBarStateController.RANK_STATUS_BAR); mBiometricUnlockController = mBiometricUnlockControllerLazy.get(); mBiometricUnlockController.addBiometricModeListener( new BiometricUnlockController.BiometricModeListener() { @@ -3379,7 +3380,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mStatusBarHideIconsForBouncerManager.setBouncerShowingAndTriggerUpdate(bouncerShowing); mCommandQueue.recomputeDisableFlags(mDisplayId, true /* animate */); if (mBouncerShowing) { - wakeUpIfDozing(SystemClock.uptimeMillis(), null, "BOUNCER_VISIBLE"); + wakeUpIfDozing(SystemClock.uptimeMillis(), null, "BOUNCER_VISIBLE", + PowerManager.WAKE_REASON_GESTURE); } updateScrimController(); if (!mBouncerShowing) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java index c7be2193e9b5..c72eb054c62c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java @@ -42,6 +42,8 @@ import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconStat import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState; import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView; import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel; +import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView; +import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel; import java.util.ArrayList; import java.util.List; @@ -56,14 +58,17 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da private final int mIconSize; private StatusBarWifiView mWifiView; + private ModernStatusBarWifiView mModernWifiView; private boolean mDemoMode; private int mColor; private final MobileIconsViewModel mMobileIconsViewModel; + private final StatusBarLocation mLocation; public DemoStatusIcons( LinearLayout statusIcons, MobileIconsViewModel mobileIconsViewModel, + StatusBarLocation location, int iconSize ) { super(statusIcons.getContext()); @@ -71,6 +76,7 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da mIconSize = iconSize; mColor = DarkIconDispatcher.DEFAULT_ICON_TINT; mMobileIconsViewModel = mobileIconsViewModel; + mLocation = location; if (statusIcons instanceof StatusIconContainer) { setShouldRestrictIcons(((StatusIconContainer) statusIcons).isRestrictingIcons()); @@ -233,14 +239,14 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da public void addDemoWifiView(WifiIconState state) { Log.d(TAG, "addDemoWifiView: "); - // TODO(b/238425913): Migrate this view to {@code ModernStatusBarWifiView}. StatusBarWifiView view = StatusBarWifiView.fromContext(mContext, state.slot); int viewIndex = getChildCount(); // If we have mobile views, put wifi before them for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); - if (child instanceof StatusBarMobileView) { + if (child instanceof StatusBarMobileView + || child instanceof ModernStatusBarMobileView) { viewIndex = i; break; } @@ -287,7 +293,7 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da ModernStatusBarMobileView view = ModernStatusBarMobileView.constructAndBind( mobileContext, "mobile", - mMobileIconsViewModel.viewModelForSub(subId) + mMobileIconsViewModel.viewModelForSub(subId, mLocation) ); // mobile always goes at the end @@ -296,6 +302,30 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da } /** + * Add a {@link ModernStatusBarWifiView} + */ + public void addModernWifiView(LocationBasedWifiViewModel viewModel) { + Log.d(TAG, "addModernDemoWifiView: "); + ModernStatusBarWifiView view = ModernStatusBarWifiView + .constructAndBind(mContext, "wifi", viewModel); + + int viewIndex = getChildCount(); + // If we have mobile views, put wifi before them + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child instanceof StatusBarMobileView + || child instanceof ModernStatusBarMobileView) { + viewIndex = i; + break; + } + } + + mModernWifiView = view; + mModernWifiView.setStaticDrawableColor(mColor); + addView(view, viewIndex, createLayoutParams()); + } + + /** * Apply an update to a mobile icon view for the given {@link MobileIconState}. For * compatibility with {@link MobileContextProvider}, we have to recreate the view every time we * update it, since the context (and thus the {@link Configuration}) may have changed @@ -317,8 +347,14 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da public void onRemoveIcon(StatusIconDisplayable view) { if (view.getSlot().equals("wifi")) { - removeView(mWifiView); - mWifiView = null; + if (view instanceof StatusBarWifiView) { + removeView(mWifiView); + mWifiView = null; + } else if (view instanceof ModernStatusBarWifiView) { + Log.d(TAG, "onRemoveIcon: removing modern wifi view"); + removeView(mModernWifiView); + mModernWifiView = null; + } } else if (view instanceof StatusBarMobileView) { StatusBarMobileView mobileView = matchingMobileView(view); if (mobileView != null) { @@ -371,8 +407,14 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da if (mWifiView != null) { mWifiView.onDarkChanged(areas, darkIntensity, tint); } + if (mModernWifiView != null) { + mModernWifiView.onDarkChanged(areas, darkIntensity, tint); + } for (StatusBarMobileView view : mMobileViews) { view.onDarkChanged(areas, darkIntensity, tint); } + for (ModernStatusBarMobileView view : mModernMobileViews) { + view.onDarkChanged(areas, darkIntensity, tint); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt index 2ce116394236..e4227dce94e7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt @@ -30,6 +30,7 @@ import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder.bind import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.VibratorHelper /** * Renders the bottom area of the lock-screen. Concerned primarily with the quick affordance UI @@ -65,12 +66,14 @@ constructor( falsingManager: FalsingManager? = null, lockIconViewController: LockIconViewController? = null, messageDisplayer: MessageDisplayer? = null, + vibratorHelper: VibratorHelper? = null, ) { binding = bind( this, viewModel, falsingManager, + vibratorHelper, ) { messageDisplayer?.display(it) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java index df3ab493a4da..1a14a0363763 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java @@ -359,6 +359,7 @@ public interface StatusBarIconController { // Whether or not these icons show up in dumpsys protected boolean mShouldLog = false; private StatusBarIconController mController; + private final StatusBarLocation mLocation; // Enables SystemUI demo mode to take effect in this group protected boolean mDemoable = true; @@ -381,11 +382,12 @@ public interface StatusBarIconController { mContext = group.getContext(); mIconSize = mContext.getResources().getDimensionPixelSize( com.android.internal.R.dimen.status_bar_icon_size); + mLocation = location; if (statusBarPipelineFlags.runNewMobileIconsBackend()) { // This starts the flow for the new pipeline, and will notify us of changes if // {@link StatusBarPipelineFlags#useNewMobileIcons} is also true. - mMobileIconsViewModel = mobileUiAdapter.createMobileIconsViewModel(); + mMobileIconsViewModel = mobileUiAdapter.getMobileIconsViewModel(); MobileIconsBinder.bind(mGroup, mMobileIconsViewModel); } else { mMobileIconsViewModel = null; @@ -394,7 +396,7 @@ public interface StatusBarIconController { if (statusBarPipelineFlags.runNewWifiIconBackend()) { // This starts the flow for the new pipeline, and will notify us of changes if // {@link StatusBarPipelineFlags#useNewWifiIcon} is also true. - mWifiViewModel = wifiUiAdapter.bindGroup(mGroup, location); + mWifiViewModel = wifiUiAdapter.bindGroup(mGroup, mLocation); } else { mWifiViewModel = null; } @@ -495,6 +497,11 @@ public interface StatusBarIconController { ModernStatusBarWifiView view = onCreateModernStatusBarWifiView(slot); mGroup.addView(view, index, onCreateLayoutParams()); + + if (mIsInDemoMode) { + mDemoStatusIcons.addModernWifiView(mWifiViewModel); + } + return view; } @@ -569,7 +576,7 @@ public interface StatusBarIconController { .constructAndBind( mobileContext, slot, - mMobileIconsViewModel.viewModelForSub(subId) + mMobileIconsViewModel.viewModelForSub(subId, mLocation) ); } @@ -686,6 +693,9 @@ public interface StatusBarIconController { mIsInDemoMode = true; if (mDemoStatusIcons == null) { mDemoStatusIcons = createDemoStatusIcons(); + if (mStatusBarPipelineFlags.useNewWifiIcon()) { + mDemoStatusIcons.addModernWifiView(mWifiViewModel); + } } mDemoStatusIcons.onDemoModeStarted(); } @@ -705,7 +715,12 @@ public interface StatusBarIconController { } protected DemoStatusIcons createDemoStatusIcons() { - return new DemoStatusIcons((LinearLayout) mGroup, mMobileIconsViewModel, mIconSize); + return new DemoStatusIcons( + (LinearLayout) mGroup, + mMobileIconsViewModel, + mLocation, + mIconSize + ); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java index a1e0c5067ef3..da1c361bced7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java @@ -20,6 +20,7 @@ import static com.android.systemui.statusbar.phone.CentralSurfaces.MULTIUSER_DEB import android.app.KeyguardManager; import android.content.Context; +import android.os.PowerManager; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; @@ -270,7 +271,8 @@ class StatusBarNotificationPresenter implements NotificationPresenter, boolean nowExpanded) { mHeadsUpManager.setExpanded(clickedEntry, nowExpanded); mCentralSurfaces.wakeUpIfDozing( - SystemClock.uptimeMillis(), clickedView, "NOTIFICATION_CLICK"); + SystemClock.uptimeMillis(), clickedView, "NOTIFICATION_CLICK", + PowerManager.WAKE_REASON_GESTURE); if (nowExpanded) { if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) { mShadeTransitionController.goToLockedShade(clickedEntry.getRow()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index c350c78913d3..0d01715715c0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -36,7 +36,7 @@ import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxyIm import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository -import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl +import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositorySwitcher import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl import dagger.Binds @@ -56,7 +56,7 @@ abstract class StatusBarPipelineModule { @Binds abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository - @Binds abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository + @Binds abstract fun wifiRepository(impl: WifiRepositorySwitcher): WifiRepository @Binds abstract fun wifiInteractor(impl: WifiInteractorImpl): WifiInteractor diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt index 6c37f94007cb..1aa954ff48cf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt @@ -26,6 +26,8 @@ import android.telephony.TelephonyCallback.ServiceStateListener import android.telephony.TelephonyCallback.SignalStrengthsListener import android.telephony.TelephonyDisplayInfo import android.telephony.TelephonyManager +import com.android.systemui.log.table.Diffable +import com.android.systemui.log.table.TableRowLogger import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Disconnected import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel @@ -79,4 +81,72 @@ data class MobileConnectionModel( * [TelephonyDisplayInfo.getNetworkType]. This is used to look up the proper network type icon */ val resolvedNetworkType: ResolvedNetworkType = ResolvedNetworkType.UnknownNetworkType, -) +) : Diffable<MobileConnectionModel> { + override fun logDiffs(prevVal: MobileConnectionModel, row: TableRowLogger) { + if (prevVal.dataConnectionState != dataConnectionState) { + row.logChange(COL_CONNECTION_STATE, dataConnectionState.toString()) + } + + if (prevVal.isEmergencyOnly != isEmergencyOnly) { + row.logChange(COL_EMERGENCY, isEmergencyOnly) + } + + if (prevVal.isRoaming != isRoaming) { + row.logChange(COL_ROAMING, isRoaming) + } + + if (prevVal.operatorAlphaShort != operatorAlphaShort) { + row.logChange(COL_OPERATOR, operatorAlphaShort) + } + + if (prevVal.isGsm != isGsm) { + row.logChange(COL_IS_GSM, isGsm) + } + + if (prevVal.cdmaLevel != cdmaLevel) { + row.logChange(COL_CDMA_LEVEL, cdmaLevel) + } + + if (prevVal.primaryLevel != primaryLevel) { + row.logChange(COL_PRIMARY_LEVEL, primaryLevel) + } + + if (prevVal.dataActivityDirection != dataActivityDirection) { + row.logChange(COL_ACTIVITY_DIRECTION, dataActivityDirection.toString()) + } + + if (prevVal.carrierNetworkChangeActive != carrierNetworkChangeActive) { + row.logChange(COL_CARRIER_NETWORK_CHANGE, carrierNetworkChangeActive) + } + + if (prevVal.resolvedNetworkType != resolvedNetworkType) { + row.logChange(COL_RESOLVED_NETWORK_TYPE, resolvedNetworkType.toString()) + } + } + + override fun logFull(row: TableRowLogger) { + row.logChange(COL_CONNECTION_STATE, dataConnectionState.toString()) + row.logChange(COL_EMERGENCY, isEmergencyOnly) + row.logChange(COL_ROAMING, isRoaming) + row.logChange(COL_OPERATOR, operatorAlphaShort) + row.logChange(COL_IS_GSM, isGsm) + row.logChange(COL_CDMA_LEVEL, cdmaLevel) + row.logChange(COL_PRIMARY_LEVEL, primaryLevel) + row.logChange(COL_ACTIVITY_DIRECTION, dataActivityDirection.toString()) + row.logChange(COL_CARRIER_NETWORK_CHANGE, carrierNetworkChangeActive) + row.logChange(COL_RESOLVED_NETWORK_TYPE, resolvedNetworkType.toString()) + } + + companion object { + const val COL_EMERGENCY = "EmergencyOnly" + const val COL_ROAMING = "Roaming" + const val COL_OPERATOR = "OperatorName" + const val COL_IS_GSM = "IsGsm" + const val COL_CDMA_LEVEL = "CdmaLevel" + const val COL_PRIMARY_LEVEL = "PrimaryLevel" + const val COL_CONNECTION_STATE = "ConnectionState" + const val COL_ACTIVITY_DIRECTION = "DataActivity" + const val COL_CARRIER_NETWORK_CHANGE = "CarrierNetworkChangeActive" + const val COL_RESOLVED_NETWORK_TYPE = "NetworkType" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt index a8cf35ad3029..c50d82a66c76 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt @@ -21,22 +21,48 @@ import android.telephony.TelephonyManager.EXTRA_DATA_SPN import android.telephony.TelephonyManager.EXTRA_PLMN import android.telephony.TelephonyManager.EXTRA_SHOW_PLMN import android.telephony.TelephonyManager.EXTRA_SHOW_SPN +import com.android.systemui.log.table.Diffable +import com.android.systemui.log.table.TableRowLogger /** * Encapsulates the data needed to show a network name for a mobile network. The data is parsed from * the intent sent by [android.telephony.TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED]. */ -sealed interface NetworkNameModel { +sealed interface NetworkNameModel : Diffable<NetworkNameModel> { val name: String /** The default name is read from [com.android.internal.R.string.lockscreen_carrier_default] */ - data class Default(override val name: String) : NetworkNameModel + data class Default(override val name: String) : NetworkNameModel { + override fun logDiffs(prevVal: NetworkNameModel, row: TableRowLogger) { + if (prevVal !is Default || prevVal.name != name) { + row.logChange(COL_NETWORK_NAME, "Default($name)") + } + } + + override fun logFull(row: TableRowLogger) { + row.logChange(COL_NETWORK_NAME, "Default($name)") + } + } /** * This name has been derived from telephony intents. see * [android.telephony.TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED] */ - data class Derived(override val name: String) : NetworkNameModel + data class Derived(override val name: String) : NetworkNameModel { + override fun logDiffs(prevVal: NetworkNameModel, row: TableRowLogger) { + if (prevVal !is Derived || prevVal.name != name) { + row.logChange(COL_NETWORK_NAME, "Derived($name)") + } + } + + override fun logFull(row: TableRowLogger) { + row.logChange(COL_NETWORK_NAME, "Derived($name)") + } + } + + companion object { + const val COL_NETWORK_NAME = "networkName" + } } fun Intent.toNetworkNameModel(separator: String): NetworkNameModel? { 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 2fd415e6777f..40e9ba1a46c7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt @@ -20,6 +20,7 @@ import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager import android.telephony.TelephonyCallback import android.telephony.TelephonyManager +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import kotlinx.coroutines.flow.Flow @@ -39,6 +40,13 @@ import kotlinx.coroutines.flow.StateFlow interface MobileConnectionRepository { /** The subscriptionId that this connection represents */ val subId: Int + + /** + * The table log buffer created for this connection. Will have the name "MobileConnectionLog + * [subId]" + */ + val tableLogBuffer: TableLogBuffer + /** * A flow that aggregates all necessary callbacks from [TelephonyCallback] into a single * listener + model. 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 d3ee85f19347..b252de8dd389 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 @@ -24,6 +24,8 @@ 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 @@ -60,6 +62,7 @@ constructor( private val dataSource: DemoModeMobileConnectionDataSource, @Application private val scope: CoroutineScope, context: Context, + private val logFactory: TableLogBufferFactory, ) : MobileConnectionsRepository { private var demoCommandJob: Job? = null @@ -149,7 +152,16 @@ constructor( override fun getRepoForSubId(subId: Int): DemoMobileConnectionRepository { return connectionRepoCache[subId] - ?: DemoMobileConnectionRepository(subId).also { connectionRepoCache[subId] = it } + ?: createDemoMobileConnectionRepo(subId).also { connectionRepoCache[subId] = it } + } + + private fun createDemoMobileConnectionRepo(subId: Int): DemoMobileConnectionRepository { + val tableLogBuffer = logFactory.create("DemoMobileConnectionLog [$subId]", 100) + + return DemoMobileConnectionRepository( + subId, + tableLogBuffer, + ) } override val globalMobileDataSettingChangedEvent = MutableStateFlow(Unit) @@ -260,7 +272,10 @@ constructor( } } -class DemoMobileConnectionRepository(override val subId: Int) : MobileConnectionRepository { +class DemoMobileConnectionRepository( + override val subId: Int, + override val tableLogBuffer: TableLogBuffer, +) : MobileConnectionRepository { override val connectionInfo = MutableStateFlow(MobileConnectionModel()) override val dataEnabled = MutableStateFlow(true) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt index a1ae8ed7329a..d4ddb856eecf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt @@ -24,10 +24,8 @@ import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE import android.telephony.TelephonyManager.DATA_ACTIVITY_OUT import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.TelephonyIcons -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.demomode.DemoMode import com.android.systemui.demomode.DemoMode.COMMAND_NETWORK import com.android.systemui.demomode.DemoModeController import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel @@ -35,8 +33,6 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled import javax.inject.Inject import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn @@ -52,27 +48,7 @@ constructor( demoModeController: DemoModeController, @Application scope: CoroutineScope, ) { - private val demoCommandStream: Flow<Bundle> = conflatedCallbackFlow { - val callback = - object : DemoMode { - override fun demoCommands(): List<String> = listOf(COMMAND_NETWORK) - - override fun dispatchDemoCommand(command: String, args: Bundle) { - trySend(args) - } - - override fun onDemoModeFinished() { - // Handled elsewhere - } - - override fun onDemoModeStarted() { - // Handled elsewhere - } - } - - demoModeController.addCallback(callback) - awaitClose { demoModeController.removeCallback(callback) } - } + private val demoCommandStream = demoModeController.demoFlowForCommand(COMMAND_NETWORK) // If the args contains "mobile", then all of the args are relevant. It's just the way demo mode // commands work and it's a little silly 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 7e9a9cea9b95..0b9e1583898e 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 @@ -36,6 +36,9 @@ import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.TableLogBufferFactory +import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType @@ -46,7 +49,6 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameMo import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger -import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel import com.android.systemui.util.settings.GlobalSettings import javax.inject.Inject @@ -59,6 +61,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach @@ -79,6 +82,7 @@ class MobileConnectionRepositoryImpl( mobileMappingsProxy: MobileMappingsProxy, bgDispatcher: CoroutineDispatcher, logger: ConnectivityPipelineLogger, + mobileLogger: TableLogBuffer, scope: CoroutineScope, ) : MobileConnectionRepository { init { @@ -92,10 +96,11 @@ class MobileConnectionRepositoryImpl( private val telephonyCallbackEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1) + override val tableLogBuffer: TableLogBuffer = mobileLogger + override val connectionInfo: StateFlow<MobileConnectionModel> = run { var state = MobileConnectionModel() conflatedCallbackFlow { - // TODO (b/240569788): log all of these into the connectivity logger val callback = object : TelephonyCallback(), @@ -106,6 +111,7 @@ class MobileConnectionRepositoryImpl( TelephonyCallback.CarrierNetworkListener, TelephonyCallback.DisplayInfoListener { override fun onServiceStateChanged(serviceState: ServiceState) { + logger.logOnServiceStateChanged(serviceState, subId) state = state.copy( isEmergencyOnly = serviceState.isEmergencyOnly, @@ -116,6 +122,7 @@ class MobileConnectionRepositoryImpl( } override fun onSignalStrengthsChanged(signalStrength: SignalStrength) { + logger.logOnSignalStrengthsChanged(signalStrength, subId) val cdmaLevel = signalStrength .getCellSignalStrengths(CellSignalStrengthCdma::class.java) @@ -142,12 +149,14 @@ class MobileConnectionRepositoryImpl( dataState: Int, networkType: Int ) { + logger.logOnDataConnectionStateChanged(dataState, networkType, subId) state = state.copy(dataConnectionState = dataState.toDataConnectionType()) trySend(state) } override fun onDataActivity(direction: Int) { + logger.logOnDataActivity(direction, subId) state = state.copy( dataActivityDirection = direction.toMobileDataActivityModel() @@ -156,6 +165,7 @@ class MobileConnectionRepositoryImpl( } override fun onCarrierNetworkChange(active: Boolean) { + logger.logOnCarrierNetworkChange(active, subId) state = state.copy(carrierNetworkChangeActive = active) trySend(state) } @@ -163,6 +173,7 @@ class MobileConnectionRepositoryImpl( override fun onDisplayInfoChanged( telephonyDisplayInfo: TelephonyDisplayInfo ) { + logger.logOnDisplayInfoChanged(telephonyDisplayInfo, subId) val networkType = if (telephonyDisplayInfo.networkType == NETWORK_TYPE_UNKNOWN) { @@ -193,7 +204,11 @@ class MobileConnectionRepositoryImpl( awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } } .onEach { telephonyCallbackEvent.tryEmit(Unit) } - .logOutputChange(logger, "MobileSubscriptionModel") + .logDiffsForTable( + mobileLogger, + columnPrefix = "MobileConnection ($subId)", + initialValue = state, + ) .stateIn(scope, SharingStarted.WhileSubscribed(), state) } @@ -243,19 +258,43 @@ class MobileConnectionRepositoryImpl( intent.toNetworkNameModel(networkNameSeparator) ?: defaultNetworkName } } + .distinctUntilChanged() + .logDiffsForTable( + mobileLogger, + columnPrefix = "", + initialValue = defaultNetworkName, + ) .stateIn(scope, SharingStarted.WhileSubscribed(), defaultNetworkName) - override val dataEnabled: StateFlow<Boolean> = + override val dataEnabled: StateFlow<Boolean> = run { + val initial = dataConnectionAllowed() telephonyPollingEvent .mapLatest { dataConnectionAllowed() } - .stateIn(scope, SharingStarted.WhileSubscribed(), dataConnectionAllowed()) + .distinctUntilChanged() + .logDiffsForTable( + mobileLogger, + columnPrefix = "", + columnName = "dataEnabled", + initialValue = initial, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), initial) + } private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed - override val isDefaultDataSubscription: StateFlow<Boolean> = + override val isDefaultDataSubscription: StateFlow<Boolean> = run { + val initialValue = defaultDataSubId.value == subId defaultDataSubId .mapLatest { it == subId } - .stateIn(scope, SharingStarted.WhileSubscribed(), defaultDataSubId.value == subId) + .distinctUntilChanged() + .logDiffsForTable( + mobileLogger, + columnPrefix = "", + columnName = "isDefaultDataSub", + initialValue = initialValue, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue) + } class Factory @Inject @@ -266,6 +305,7 @@ class MobileConnectionRepositoryImpl( private val logger: ConnectivityPipelineLogger, private val globalSettings: GlobalSettings, private val mobileMappingsProxy: MobileMappingsProxy, + private val logFactory: TableLogBufferFactory, @Background private val bgDispatcher: CoroutineDispatcher, @Application private val scope: CoroutineScope, ) { @@ -276,6 +316,8 @@ class MobileConnectionRepositoryImpl( defaultDataSubId: StateFlow<Int>, globalMobileDataSettingChangedEvent: Flow<Unit>, ): MobileConnectionRepository { + val mobileLogger = logFactory.create(tableBufferLogName(subId), 100) + return MobileConnectionRepositoryImpl( context, subId, @@ -289,8 +331,13 @@ class MobileConnectionRepositoryImpl( mobileMappingsProxy, bgDispatcher, logger, + mobileLogger, scope, ) } } + + companion object { + fun tableBufferLogName(subId: Int): String = "MobileConnectionLog [$subId]" + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt index a9b3d18774fd..d407abeb2315 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 @@ -51,6 +51,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConn import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange import com.android.systemui.util.settings.GlobalSettings import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -120,6 +121,7 @@ constructor( awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) } } .mapLatest { fetchSubscriptionsList().map { it.toSubscriptionModel() } } + .logInputChange(logger, "onSubscriptionsChanged") .onEach { infos -> dropUnusedReposFromCache(infos) } .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf()) @@ -136,6 +138,8 @@ constructor( telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } } + .distinctUntilChanged() + .logInputChange(logger, "onActiveDataSubscriptionIdChanged") .stateIn(scope, started = SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID) private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> = @@ -149,6 +153,7 @@ constructor( intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID) } .distinctUntilChanged() + .logInputChange(logger, "ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED") .onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) } .stateIn( scope, @@ -157,13 +162,15 @@ constructor( ) private val carrierConfigChangedEvent = - broadcastDispatcher.broadcastFlow( - IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED) - ) + broadcastDispatcher + .broadcastFlow(IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)) + .logInputChange(logger, "ACTION_CARRIER_CONFIG_CHANGED") override val defaultDataSubRatConfig: StateFlow<Config> = merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent) .mapLatest { Config.readConfig(context) } + .distinctUntilChanged() + .logInputChange(logger, "defaultDataSubRatConfig") .stateIn( scope, SharingStarted.WhileSubscribed(), @@ -171,10 +178,16 @@ constructor( ) override val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>> = - defaultDataSubRatConfig.map { mobileMappingsProxy.mapIconSets(it) } + defaultDataSubRatConfig + .map { mobileMappingsProxy.mapIconSets(it) } + .distinctUntilChanged() + .logInputChange(logger, "defaultMobileIconMapping") override val defaultMobileIconGroup: Flow<MobileIconGroup> = - defaultDataSubRatConfig.map { mobileMappingsProxy.getDefaultIcons(it) } + defaultDataSubRatConfig + .map { mobileMappingsProxy.getDefaultIcons(it) } + .distinctUntilChanged() + .logInputChange(logger, "defaultMobileIconGroup") override fun getRepoForSubId(subId: Int): MobileConnectionRepository { if (!isValidSubId(subId)) { @@ -191,22 +204,24 @@ constructor( * In single-SIM devices, the [MOBILE_DATA] setting is phone-wide. For multi-SIM, the individual * connection repositories also observe the URI for [MOBILE_DATA] + subId. */ - override val globalMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow { - val observer = - object : ContentObserver(null) { - override fun onChange(selfChange: Boolean) { - trySend(Unit) - } - } + override val globalMobileDataSettingChangedEvent: Flow<Unit> = + conflatedCallbackFlow { + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(Unit) + } + } - globalSettings.registerContentObserver( - globalSettings.getUriFor(MOBILE_DATA), - true, - observer - ) + globalSettings.registerContentObserver( + globalSettings.getUriFor(MOBILE_DATA), + true, + observer + ) - awaitClose { context.contentResolver.unregisterContentObserver(observer) } - } + awaitClose { context.contentResolver.unregisterContentObserver(observer) } + } + .logInputChange(logger, "globalMobileDataSettingChangedEvent") @SuppressLint("MissingPermission") override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> = @@ -236,6 +251,8 @@ constructor( awaitClose { connectivityManager.unregisterNetworkCallback(callback) } } + .distinctUntilChanged() + .logInputChange(logger, "defaultMobileNetworkConnectivity") .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectivityModel()) private fun isValidSubId(subId: Int): Boolean { 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 76e6a96a19d7..e6686dce7bbc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.telephony.CarrierConfigManager import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository @@ -35,6 +36,9 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn interface MobileIconInteractor { + /** The table log created for this connection */ + val tableLogBuffer: TableLogBuffer + /** The current mobile data activity */ val activity: Flow<DataActivityModel> @@ -97,6 +101,8 @@ class MobileIconInteractorImpl( ) : MobileIconInteractor { private val connectionInfo = connectionRepository.connectionInfo + override val tableLogBuffer: TableLogBuffer = connectionRepository.tableLogBuffer + override val activity = connectionInfo.mapLatest { it.dataActivityDirection } override val isDataEnabled: StateFlow<Boolean> = connectionRepository.dataEnabled 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 62fa723dbf04..829a5cad6504 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt @@ -20,7 +20,6 @@ import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.statusbar.phone.StatusBarIconController -import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel @@ -70,6 +69,9 @@ constructor( private val mobileSubIdsState: StateFlow<List<Int>> = mobileSubIds.stateIn(scope, SharingStarted.WhileSubscribed(), listOf()) + /** In order to keep the logs tame, we will reuse the same top-level mobile icons view model */ + val mobileIconsViewModel = iconsViewModelFactory.create(mobileSubIdsState) + override fun start() { // Only notify the icon controller if we want to *render* the new icons. // Note that this flow may still run if @@ -81,12 +83,4 @@ constructor( } } } - - /** - * Create a MobileIconsViewModel for a given [IconManager], and bind it to to the manager's - * lifecycle. This will start collecting on [mobileSubIdsState] and link our new pipeline with - * the old view system. - */ - fun createMobileIconsViewModel(): MobileIconsViewModel = - iconsViewModelFactory.create(mobileSubIdsState) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt index 545e624273f1..ab442b5ab4de 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 @@ -30,7 +30,7 @@ import com.android.settingslib.graph.SignalDrawable import com.android.systemui.R import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch @@ -39,7 +39,7 @@ object MobileIconBinder { @JvmStatic fun bind( view: ViewGroup, - viewModel: MobileIconViewModel, + viewModel: LocationBasedMobileViewModel, ) { val activityContainer = view.requireViewById<View>(R.id.inout_container) val activityIn = view.requireViewById<ImageView>(R.id.mobile_in) 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 0ab7bcd96844..e86fee24fe4d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt @@ -24,7 +24,7 @@ import com.android.systemui.R import com.android.systemui.statusbar.BaseStatusBarFrameLayout import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinder -import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel import java.util.ArrayList class ModernStatusBarMobileView( @@ -71,7 +71,7 @@ class ModernStatusBarMobileView( fun constructAndBind( context: Context, slot: String, - viewModel: MobileIconViewModel, + viewModel: LocationBasedMobileViewModel, ): ModernStatusBarMobileView { return (LayoutInflater.from(context) .inflate(R.layout.status_bar_mobile_signal_group_new, null) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt new file mode 100644 index 000000000000..b0dc41f45488 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel + +import android.graphics.Color +import com.android.systemui.statusbar.phone.StatusBarLocation +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOf + +/** + * A view model for an individual mobile icon that embeds the notion of a [StatusBarLocation]. This + * allows the mobile icon to change some view parameters at different locations + * + * @param commonImpl for convenience, this class wraps a base interface that can provides all of the + * common implementations between locations. See [MobileIconViewModel] + */ +abstract class LocationBasedMobileViewModel( + val commonImpl: MobileIconViewModelCommon, + val logger: ConnectivityPipelineLogger, +) : MobileIconViewModelCommon by commonImpl { + abstract val tint: Flow<Int> + + companion object { + fun viewModelForLocation( + commonImpl: MobileIconViewModelCommon, + logger: ConnectivityPipelineLogger, + loc: StatusBarLocation, + ): LocationBasedMobileViewModel = + when (loc) { + StatusBarLocation.HOME -> HomeMobileIconViewModel(commonImpl, logger) + StatusBarLocation.KEYGUARD -> KeyguardMobileIconViewModel(commonImpl, logger) + StatusBarLocation.QS -> QsMobileIconViewModel(commonImpl, logger) + } + } +} + +class HomeMobileIconViewModel( + commonImpl: MobileIconViewModelCommon, + logger: ConnectivityPipelineLogger, +) : MobileIconViewModelCommon, LocationBasedMobileViewModel(commonImpl, logger) { + override val tint: Flow<Int> = + flowOf(Color.CYAN) + .distinctUntilChanged() + .logOutputChange(logger, "HOME tint(${commonImpl.subscriptionId})") +} + +class QsMobileIconViewModel( + commonImpl: MobileIconViewModelCommon, + logger: ConnectivityPipelineLogger, +) : MobileIconViewModelCommon, LocationBasedMobileViewModel(commonImpl, logger) { + override val tint: Flow<Int> = + flowOf(Color.GREEN) + .distinctUntilChanged() + .logOutputChange(logger, "QS tint(${commonImpl.subscriptionId})") +} + +class KeyguardMobileIconViewModel( + commonImpl: MobileIconViewModelCommon, + logger: ConnectivityPipelineLogger, +) : MobileIconViewModelCommon, LocationBasedMobileViewModel(commonImpl, logger) { + override val tint: Flow<Int> = + flowOf(Color.MAGENTA) + .distinctUntilChanged() + .logOutputChange(logger, "KEYGUARD tint(${commonImpl.subscriptionId})") +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt index 961283f57def..2d6ac4efd512 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt @@ -16,23 +16,40 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel -import android.graphics.Color import com.android.settingslib.graph.SignalDrawable import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon +import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger -import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn + +/** Common interface for all of the location-based mobile icon view models. */ +interface MobileIconViewModelCommon { + val subscriptionId: Int + /** An int consumable by [SignalDrawable] for display */ + val iconId: Flow<Int> + val roaming: Flow<Boolean> + /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */ + val networkTypeIcon: Flow<Icon?> + val activityInVisible: Flow<Boolean> + val activityOutVisible: Flow<Boolean> + val activityContainerVisible: Flow<Boolean> +} /** * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over @@ -40,25 +57,29 @@ import kotlinx.coroutines.flow.mapLatest * subscription's information. * * There will be exactly one [MobileIconViewModel] per filtered subscription offered from - * [MobileIconsInteractor.filteredSubscriptions] + * [MobileIconsInteractor.filteredSubscriptions]. * - * TODO: figure out where carrier merged and VCN models go (probably here?) + * For the sake of keeping log spam in check, every flow funding the [MobileIconViewModelCommon] + * interface is implemented as a [StateFlow]. This ensures that each location-based mobile icon view + * model gets the exact same information, as well as allows us to log that unified state only once + * per icon. */ @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) class MobileIconViewModel constructor( - val subscriptionId: Int, + override val subscriptionId: Int, iconInteractor: MobileIconInteractor, logger: ConnectivityPipelineLogger, constants: ConnectivityConstants, -) { + scope: CoroutineScope, +) : MobileIconViewModelCommon { /** Whether or not to show the error state of [SignalDrawable] */ private val showExclamationMark: Flow<Boolean> = iconInteractor.isDefaultDataEnabled.mapLatest { !it } - /** An int consumable by [SignalDrawable] for display */ - val iconId: Flow<Int> = + override val iconId: Flow<Int> = run { + val initial = SignalDrawable.getEmptyState(iconInteractor.numberOfLevels.value) combine(iconInteractor.level, iconInteractor.numberOfLevels, showExclamationMark) { level, numberOfLevels, @@ -66,32 +87,56 @@ constructor( SignalDrawable.getState(level, numberOfLevels, showExclamationMark) } .distinctUntilChanged() - .logOutputChange(logger, "iconId($subscriptionId)") + .logDiffsForTable( + iconInteractor.tableLogBuffer, + columnPrefix = "", + columnName = "iconId", + initialValue = initial, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), initial) + } - /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */ - val networkTypeIcon: Flow<Icon?> = + override val networkTypeIcon: Flow<Icon?> = combine( - iconInteractor.networkTypeIconGroup, - iconInteractor.isDataConnected, - iconInteractor.isDataEnabled, - iconInteractor.isDefaultConnectionFailed, - iconInteractor.alwaysShowDataRatIcon, - ) { networkTypeIconGroup, dataConnected, dataEnabled, failedConnection, alwaysShow -> - val desc = - if (networkTypeIconGroup.dataContentDescription != 0) - ContentDescription.Resource(networkTypeIconGroup.dataContentDescription) - else null - val icon = Icon.Resource(networkTypeIconGroup.dataType, desc) - return@combine when { - alwaysShow -> icon - !dataConnected -> null - !dataEnabled -> null - failedConnection -> null - else -> icon + iconInteractor.networkTypeIconGroup, + iconInteractor.isDataConnected, + iconInteractor.isDataEnabled, + iconInteractor.isDefaultConnectionFailed, + iconInteractor.alwaysShowDataRatIcon, + ) { networkTypeIconGroup, dataConnected, dataEnabled, failedConnection, alwaysShow -> + val desc = + if (networkTypeIconGroup.dataContentDescription != 0) + ContentDescription.Resource(networkTypeIconGroup.dataContentDescription) + else null + val icon = Icon.Resource(networkTypeIconGroup.dataType, desc) + return@combine when { + alwaysShow -> icon + !dataConnected -> null + !dataEnabled -> null + failedConnection -> null + else -> icon + } } - } + .distinctUntilChanged() + .onEach { + // This is done as an onEach side effect since Icon is not Diffable (yet) + iconInteractor.tableLogBuffer.logChange( + prefix = "", + columnName = "networkTypeIcon", + value = it.toString(), + ) + } + .stateIn(scope, SharingStarted.WhileSubscribed(), null) - val roaming: Flow<Boolean> = iconInteractor.isRoaming + override val roaming: StateFlow<Boolean> = + iconInteractor.isRoaming + .logDiffsForTable( + iconInteractor.tableLogBuffer, + columnPrefix = "", + columnName = "roaming", + initialValue = false, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) private val activity: Flow<DataActivityModel?> = if (!constants.shouldShowActivityConfig) { @@ -100,10 +145,39 @@ constructor( iconInteractor.activity } - val activityInVisible: Flow<Boolean> = activity.map { it?.hasActivityIn ?: false } - val activityOutVisible: Flow<Boolean> = activity.map { it?.hasActivityOut ?: false } - val activityContainerVisible: Flow<Boolean> = - activity.map { it != null && (it.hasActivityIn || it.hasActivityOut) } + override val activityInVisible: Flow<Boolean> = + activity + .map { it?.hasActivityIn ?: false } + .distinctUntilChanged() + .logDiffsForTable( + iconInteractor.tableLogBuffer, + columnPrefix = "", + columnName = "activityInVisible", + initialValue = false, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val activityOutVisible: Flow<Boolean> = + activity + .map { it?.hasActivityOut ?: false } + .distinctUntilChanged() + .logDiffsForTable( + iconInteractor.tableLogBuffer, + columnPrefix = "", + columnName = "activityOutVisible", + initialValue = false, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) - val tint: Flow<Int> = flowOf(Color.CYAN) + override val activityContainerVisible: Flow<Boolean> = + activity + .map { it != null && (it.hasActivityIn || it.hasActivityOut) } + .distinctUntilChanged() + .logDiffsForTable( + iconInteractor.tableLogBuffer, + columnPrefix = "", + columnName = "activityContainerVisible", + initialValue = false, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt index 0b41d319f9dc..b9318b181aaf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt @@ -14,17 +14,19 @@ * limitations under the License. */ -@file:OptIn(InternalCoroutinesApi::class) - package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel +import androidx.annotation.VisibleForTesting +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import javax.inject.Inject -import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch /** * View model for describing the system's current mobile cellular connections. The result is a list @@ -38,15 +40,33 @@ constructor( private val interactor: MobileIconsInteractor, private val logger: ConnectivityPipelineLogger, private val constants: ConnectivityConstants, + @Application private val scope: CoroutineScope, ) { - /** TODO: do we need to cache these? */ - fun viewModelForSub(subId: Int): MobileIconViewModel = - MobileIconViewModel( - subId, - interactor.createMobileConnectionInteractorForSubId(subId), - logger, - constants, - ) + @VisibleForTesting val mobileIconSubIdCache = mutableMapOf<Int, MobileIconViewModel>() + + init { + scope.launch { subscriptionIdsFlow.collect { removeInvalidModelsFromCache(it) } } + } + + fun viewModelForSub(subId: Int, location: StatusBarLocation): LocationBasedMobileViewModel { + val common = + mobileIconSubIdCache[subId] + ?: MobileIconViewModel( + subId, + interactor.createMobileConnectionInteractorForSubId(subId), + logger, + constants, + scope, + ) + .also { mobileIconSubIdCache[subId] = it } + + return LocationBasedMobileViewModel.viewModelForLocation(common, logger, location) + } + + private fun removeInvalidModelsFromCache(subIds: List<Int>) { + val subIdsToRemove = mobileIconSubIdCache.keys.filter { !subIds.contains(it) } + subIdsToRemove.forEach { mobileIconSubIdCache.remove(it) } + } class Factory @Inject @@ -54,6 +74,7 @@ constructor( private val interactor: MobileIconsInteractor, private val logger: ConnectivityPipelineLogger, private val constants: ConnectivityConstants, + @Application private val scope: CoroutineScope, ) { fun create(subscriptionIdsFlow: StateFlow<List<Int>>): MobileIconsViewModel { return MobileIconsViewModel( @@ -61,6 +82,7 @@ constructor( interactor, logger, constants, + scope, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt index d3cf32fb44ce..d3ff3573dae2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt @@ -18,8 +18,11 @@ package com.android.systemui.statusbar.pipeline.shared import android.net.Network import android.net.NetworkCapabilities -import com.android.systemui.log.dagger.StatusBarConnectivityLog +import android.telephony.ServiceState +import android.telephony.SignalStrength +import android.telephony.TelephonyDisplayInfo import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.log.dagger.StatusBarConnectivityLog import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.toString @@ -28,7 +31,9 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.onEach @SysUISingleton -class ConnectivityPipelineLogger @Inject constructor( +class ConnectivityPipelineLogger +@Inject +constructor( @StatusBarConnectivityLog private val buffer: LogBuffer, ) { /** @@ -37,34 +42,23 @@ class ConnectivityPipelineLogger @Inject constructor( * Use this method for inputs that don't have any extra information besides their callback name. */ fun logInputChange(callbackName: String) { - buffer.log( - SB_LOGGING_TAG, - LogLevel.INFO, - { str1 = callbackName }, - { "Input: $str1" } - ) + buffer.log(SB_LOGGING_TAG, LogLevel.INFO, { str1 = callbackName }, { "Input: $str1" }) } - /** - * Logs a change in one of the **raw inputs** to the connectivity pipeline. - */ + /** Logs a change in one of the **raw inputs** to the connectivity pipeline. */ fun logInputChange(callbackName: String, changeInfo: String?) { buffer.log( - SB_LOGGING_TAG, - LogLevel.INFO, - { - str1 = callbackName - str2 = changeInfo - }, - { - "Input: $str1: $str2" - } + SB_LOGGING_TAG, + LogLevel.INFO, + { + str1 = callbackName + str2 = changeInfo + }, + { "Input: $str1: $str2" } ) } - /** - * Logs a **data transformation** that we performed within the connectivity pipeline. - */ + /** Logs a **data transformation** that we performed within the connectivity pipeline. */ fun logTransformation(transformationName: String, oldValue: Any?, newValue: Any?) { if (oldValue == newValue) { buffer.log( @@ -74,9 +68,7 @@ class ConnectivityPipelineLogger @Inject constructor( str1 = transformationName str2 = oldValue.toString() }, - { - "Transform: $str1: $str2 (transformation didn't change it)" - } + { "Transform: $str1: $str2 (transformation didn't change it)" } ) } else { buffer.log( @@ -87,27 +79,21 @@ class ConnectivityPipelineLogger @Inject constructor( str2 = oldValue.toString() str3 = newValue.toString() }, - { - "Transform: $str1: $str2 -> $str3" - } + { "Transform: $str1: $str2 -> $str3" } ) } } - /** - * Logs a change in one of the **outputs** to the connectivity pipeline. - */ + /** Logs a change in one of the **outputs** to the connectivity pipeline. */ fun logOutputChange(outputParamName: String, changeInfo: String) { buffer.log( - SB_LOGGING_TAG, - LogLevel.INFO, - { - str1 = outputParamName - str2 = changeInfo - }, - { - "Output: $str1: $str2" - } + SB_LOGGING_TAG, + LogLevel.INFO, + { + str1 = outputParamName + str2 = changeInfo + }, + { "Output: $str1: $str2" } ) } @@ -119,9 +105,7 @@ class ConnectivityPipelineLogger @Inject constructor( int1 = network.getNetId() str1 = networkCapabilities.toString() }, - { - "onCapabilitiesChanged: net=$int1 capabilities=$str1" - } + { "onCapabilitiesChanged: net=$int1 capabilities=$str1" } ) } @@ -129,21 +113,93 @@ class ConnectivityPipelineLogger @Inject constructor( buffer.log( SB_LOGGING_TAG, LogLevel.INFO, + { int1 = network.getNetId() }, + { "onLost: net=$int1" } + ) + } + + fun logOnServiceStateChanged(serviceState: ServiceState, subId: Int) { + buffer.log( + SB_LOGGING_TAG, + LogLevel.INFO, { - int1 = network.getNetId() + int1 = subId + bool1 = serviceState.isEmergencyOnly + bool2 = serviceState.roaming + str1 = serviceState.operatorAlphaShort }, { - "onLost: net=$int1" + "onServiceStateChanged: subId=$int1 emergencyOnly=$bool1 roaming=$bool2" + + " operator=$str1" } ) } + fun logOnSignalStrengthsChanged(signalStrength: SignalStrength, subId: Int) { + buffer.log( + SB_LOGGING_TAG, + LogLevel.INFO, + { + int1 = subId + str1 = signalStrength.toString() + }, + { "onSignalStrengthsChanged: subId=$int1 strengths=$str1" } + ) + } + + fun logOnDataConnectionStateChanged(dataState: Int, networkType: Int, subId: Int) { + buffer.log( + SB_LOGGING_TAG, + LogLevel.INFO, + { + int1 = subId + int2 = dataState + str1 = networkType.toString() + }, + { "onDataConnectionStateChanged: subId=$int1 dataState=$int2 networkType=$str1" }, + ) + } + + fun logOnDataActivity(direction: Int, subId: Int) { + buffer.log( + SB_LOGGING_TAG, + LogLevel.INFO, + { + int1 = subId + int2 = direction + }, + { "onDataActivity: subId=$int1 direction=$int2" }, + ) + } + + fun logOnCarrierNetworkChange(active: Boolean, subId: Int) { + buffer.log( + SB_LOGGING_TAG, + LogLevel.INFO, + { + int1 = subId + bool1 = active + }, + { "onCarrierNetworkChange: subId=$int1 active=$bool1" }, + ) + } + + fun logOnDisplayInfoChanged(displayInfo: TelephonyDisplayInfo, subId: Int) { + buffer.log( + SB_LOGGING_TAG, + LogLevel.INFO, + { + int1 = subId + str1 = displayInfo.toString() + }, + { "onDisplayInfoChanged: subId=$int1 displayInfo=$str1" }, + ) + } + companion object { const val SB_LOGGING_TAG = "SbConnectivity" - /** - * Log a change in one of the **inputs** to the connectivity pipeline. - */ + /** Log a change in one of the **inputs** to the connectivity pipeline. */ fun Flow<Unit>.logInputChange( logger: ConnectivityPipelineLogger, inputParamName: String, @@ -155,26 +211,26 @@ class ConnectivityPipelineLogger @Inject constructor( * Log a change in one of the **inputs** to the connectivity pipeline. * * @param prettyPrint an optional function to transform the value into a readable string. - * [toString] is used if no custom function is provided. + * [toString] is used if no custom function is provided. */ fun <T> Flow<T>.logInputChange( logger: ConnectivityPipelineLogger, inputParamName: String, prettyPrint: (T) -> String = { it.toString() } ): Flow<T> { - return this.onEach {logger.logInputChange(inputParamName, prettyPrint(it)) } + return this.onEach { logger.logInputChange(inputParamName, prettyPrint(it)) } } /** * Log a change in one of the **outputs** to the connectivity pipeline. * * @param prettyPrint an optional function to transform the value into a readable string. - * [toString] is used if no custom function is provided. + * [toString] is used if no custom function is provided. */ fun <T> Flow<T>.logOutputChange( - logger: ConnectivityPipelineLogger, - outputParamName: String, - prettyPrint: (T) -> String = { it.toString() } + logger: ConnectivityPipelineLogger, + outputParamName: String, + prettyPrint: (T) -> String = { it.toString() } ): Flow<T> { return this.onEach { logger.logOutputChange(outputParamName, prettyPrint(it)) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt new file mode 100644 index 000000000000..73bcdfd2b78e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.wifi.data.repository + +import android.os.Bundle +import androidx.annotation.VisibleForTesting +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.demomode.DemoMode +import com.android.systemui.demomode.DemoModeController +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoWifiRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn + +/** + * Provides the [WifiRepository] interface either through the [DemoWifiRepository] implementation, + * or the [WifiRepositoryImpl]'s prod implementation, based on the current demo mode value. In this + * way, downstream clients can all consist of real implementations and not care about which + * repository is responsible for the data. Graphically: + * + * ``` + * RealRepository + * │ + * ├──►RepositorySwitcher──►RealInteractor──►RealViewModel + * │ + * DemoRepository + * ``` + * + * When demo mode turns on, every flow will [flatMapLatest] to the current provider's version of + * that flow. + */ +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +class WifiRepositorySwitcher +@Inject +constructor( + private val realImpl: WifiRepositoryImpl, + private val demoImpl: DemoWifiRepository, + private val demoModeController: DemoModeController, + @Application scope: CoroutineScope, +) : WifiRepository { + private val isDemoMode = + conflatedCallbackFlow { + val callback = + object : DemoMode { + override fun dispatchDemoCommand(command: String?, args: Bundle?) { + // Don't care + } + + override fun onDemoModeStarted() { + demoImpl.startProcessingCommands() + trySend(true) + } + + override fun onDemoModeFinished() { + demoImpl.stopProcessingCommands() + trySend(false) + } + } + + demoModeController.addCallback(callback) + awaitClose { demoModeController.removeCallback(callback) } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), demoModeController.isInDemoMode) + + @VisibleForTesting + val activeRepo = + isDemoMode + .mapLatest { isDemoMode -> + if (isDemoMode) { + demoImpl + } else { + realImpl + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl) + + override val isWifiEnabled: StateFlow<Boolean> = + activeRepo + .flatMapLatest { it.isWifiEnabled } + .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.isWifiEnabled.value) + + override val isWifiDefault: StateFlow<Boolean> = + activeRepo + .flatMapLatest { it.isWifiDefault } + .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.isWifiDefault.value) + + override val wifiNetwork: StateFlow<WifiNetworkModel> = + activeRepo + .flatMapLatest { it.wifiNetwork } + .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.wifiNetwork.value) + + override val wifiActivity: StateFlow<DataActivityModel> = + activeRepo + .flatMapLatest { it.wifiActivity } + .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.wifiActivity.value) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt new file mode 100644 index 000000000000..c588945fbd67 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.wifi.data.repository.demo + +import android.net.wifi.WifiManager +import android.os.Bundle +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.demomode.DemoMode.COMMAND_NETWORK +import com.android.systemui.demomode.DemoModeController +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.shareIn + +/** Data source to map between demo mode commands and inputs into [DemoWifiRepository]'s flows */ +@SysUISingleton +class DemoModeWifiDataSource +@Inject +constructor( + demoModeController: DemoModeController, + @Application scope: CoroutineScope, +) { + private val demoCommandStream = demoModeController.demoFlowForCommand(COMMAND_NETWORK) + private val _wifiCommands = demoCommandStream.map { args -> args.toWifiEvent() } + val wifiEvents = _wifiCommands.shareIn(scope, SharingStarted.WhileSubscribed()) + + private fun Bundle.toWifiEvent(): FakeWifiEventModel? { + val wifi = getString("wifi") ?: return null + return if (wifi == "show") { + activeWifiEvent() + } else { + FakeWifiEventModel.WifiDisabled + } + } + + private fun Bundle.activeWifiEvent(): FakeWifiEventModel.Wifi { + val level = getString("level")?.toInt() + val activity = getString("activity")?.toActivity() + val ssid = getString("ssid") + val validated = getString("fully").toBoolean() + + return FakeWifiEventModel.Wifi( + level = level, + activity = activity, + ssid = ssid, + validated = validated, + ) + } + + private fun String.toActivity(): Int = + when (this) { + "inout" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT + "in" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_IN + "out" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_OUT + else -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_NONE + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt new file mode 100644 index 000000000000..7890074cf8a2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.wifi.data.repository.demo + +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel +import com.android.systemui.statusbar.pipeline.shared.data.model.toWifiDataActivityModel +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch + +/** Demo-able wifi repository to support SystemUI demo mode commands. */ +class DemoWifiRepository +@Inject +constructor( + private val dataSource: DemoModeWifiDataSource, + @Application private val scope: CoroutineScope, +) : WifiRepository { + private var demoCommandJob: Job? = null + + private val _isWifiEnabled = MutableStateFlow(false) + override val isWifiEnabled: StateFlow<Boolean> = _isWifiEnabled + + private val _isWifiDefault = MutableStateFlow(false) + override val isWifiDefault: StateFlow<Boolean> = _isWifiDefault + + private val _wifiNetwork = MutableStateFlow<WifiNetworkModel>(WifiNetworkModel.Inactive) + override val wifiNetwork: StateFlow<WifiNetworkModel> = _wifiNetwork + + private val _wifiActivity = + MutableStateFlow(DataActivityModel(hasActivityIn = false, hasActivityOut = false)) + override val wifiActivity: StateFlow<DataActivityModel> = _wifiActivity + + fun startProcessingCommands() { + demoCommandJob = + scope.launch { + dataSource.wifiEvents.filterNotNull().collect { event -> processEvent(event) } + } + } + + fun stopProcessingCommands() { + demoCommandJob?.cancel() + } + + private fun processEvent(event: FakeWifiEventModel) = + when (event) { + is FakeWifiEventModel.Wifi -> processEnabledWifiState(event) + is FakeWifiEventModel.WifiDisabled -> processDisabledWifiState() + } + + private fun processDisabledWifiState() { + _isWifiEnabled.value = false + _isWifiDefault.value = false + _wifiActivity.value = DataActivityModel(hasActivityIn = false, hasActivityOut = false) + _wifiNetwork.value = WifiNetworkModel.Inactive + } + + private fun processEnabledWifiState(event: FakeWifiEventModel.Wifi) { + _isWifiEnabled.value = true + _isWifiDefault.value = true + _wifiActivity.value = + event.activity?.toWifiDataActivityModel() + ?: DataActivityModel(hasActivityIn = false, hasActivityOut = false) + _wifiNetwork.value = event.toWifiNetworkModel() + } + + private fun FakeWifiEventModel.Wifi.toWifiNetworkModel(): WifiNetworkModel = + WifiNetworkModel.Active( + networkId = DEMO_NET_ID, + isValidated = validated ?: true, + level = level, + ssid = ssid, + + // These fields below aren't supported in demo mode, since they aren't needed to satisfy + // the interface. + isPasspointAccessPoint = false, + isOnlineSignUpForPasspointAccessPoint = false, + passpointProviderFriendlyName = null, + ) + + companion object { + private const val DEMO_NET_ID = 1234 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt new file mode 100644 index 000000000000..2353fb82f3b1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model + +/** + * Model for demo wifi commands, ported from [NetworkControllerImpl] + * + * Nullable fields represent optional command line arguments + */ +sealed interface FakeWifiEventModel { + data class Wifi( + val level: Int?, + val activity: Int?, + val ssid: String?, + val validated: Boolean?, + ) : FakeWifiEventModel + + object WifiDisabled : FakeWifiEventModel +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt index 3c0eb910ad89..4f7fe28c1e7c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt @@ -38,16 +38,12 @@ class WifiConstants @Inject constructor( dumpManager.registerDumpable("${SB_LOGGING_TAG}WifiConstants", this) } - /** True if we should show the activityIn/activityOut icons and false otherwise. */ - val shouldShowActivityConfig = context.resources.getBoolean(R.bool.config_showActivity) - /** True if we should always show the wifi icon when wifi is enabled and false otherwise. */ val alwaysShowIconIfEnabled = context.resources.getBoolean(R.bool.config_showWifiIndicatorWhenEnabled) override fun dump(pw: PrintWriter, args: Array<out String>) { pw.apply { - println("shouldShowActivityConfig=$shouldShowActivityConfig") println("alwaysShowIconIfEnabled=$alwaysShowIconIfEnabled") } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt index 07a7595a2e00..ab464cc78905 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt @@ -148,7 +148,7 @@ constructor( /** The wifi activity status. Null if we shouldn't display the activity status. */ private val activity: Flow<DataActivityModel?> = - if (!wifiConstants.shouldShowActivityConfig) { + if (!connectivityConstants.shouldShowActivityConfig) { flowOf(null) } else { combine(interactor.activity, interactor.ssid) { activity, ssid -> diff --git a/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java b/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java index 1f444340653d..246488600eef 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java @@ -33,6 +33,7 @@ import com.android.systemui.R; import com.android.systemui.demomode.DemoMode; import com.android.systemui.demomode.DemoModeAvailabilityTracker; import com.android.systemui.demomode.DemoModeController; +import com.android.systemui.util.settings.GlobalSettings; public class DemoModeFragment extends PreferenceFragment implements OnPreferenceChangeListener { @@ -54,13 +55,15 @@ public class DemoModeFragment extends PreferenceFragment implements OnPreference private SwitchPreference mOnSwitch; private DemoModeController mDemoModeController; + private GlobalSettings mGlobalSettings; private Tracker mDemoModeTracker; // We are the only ones who ever call this constructor, so don't worry about the warning @SuppressLint("ValidFragment") - public DemoModeFragment(DemoModeController demoModeController) { + public DemoModeFragment(DemoModeController demoModeController, GlobalSettings globalSettings) { super(); mDemoModeController = demoModeController; + mGlobalSettings = globalSettings; } @@ -80,7 +83,7 @@ public class DemoModeFragment extends PreferenceFragment implements OnPreference screen.addPreference(mOnSwitch); setPreferenceScreen(screen); - mDemoModeTracker = new Tracker(context); + mDemoModeTracker = new Tracker(context, mGlobalSettings); mDemoModeTracker.startTracking(); updateDemoModeEnabled(); updateDemoModeOn(); @@ -202,8 +205,8 @@ public class DemoModeFragment extends PreferenceFragment implements OnPreference } private class Tracker extends DemoModeAvailabilityTracker { - Tracker(Context context) { - super(context); + Tracker(Context context, GlobalSettings globalSettings) { + super(context, globalSettings); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java index 3231aecdc4b2..32ecb6786a51 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java @@ -33,6 +33,7 @@ import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.fragments.FragmentService; +import com.android.systemui.util.settings.GlobalSettings; import javax.inject.Inject; @@ -44,12 +45,18 @@ public class TunerActivity extends Activity implements private final DemoModeController mDemoModeController; private final TunerService mTunerService; + private final GlobalSettings mGlobalSettings; @Inject - TunerActivity(DemoModeController demoModeController, TunerService tunerService) { + TunerActivity( + DemoModeController demoModeController, + TunerService tunerService, + GlobalSettings globalSettings + ) { super(); mDemoModeController = demoModeController; mTunerService = tunerService; + mGlobalSettings = globalSettings; } protected void onCreate(Bundle savedInstanceState) { @@ -69,7 +76,7 @@ public class TunerActivity extends Activity implements boolean showDemoMode = action != null && action.equals( "com.android.settings.action.DEMO_MODE"); final PreferenceFragment fragment = showDemoMode - ? new DemoModeFragment(mDemoModeController) + ? new DemoModeFragment(mDemoModeController, mGlobalSettings) : new TunerFragment(mTunerService); getFragmentManager().beginTransaction().replace(R.id.content_frame, fragment, TAG_TUNER).commit(); diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt index 9653985cb6e6..d6b3b22bfd02 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt @@ -21,6 +21,8 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.lifecycle.repeatWhenAttached import java.util.function.Consumer +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect @@ -34,7 +36,10 @@ fun <T> collectFlow( view: View, flow: Flow<T>, consumer: Consumer<T>, + coroutineContext: CoroutineContext = EmptyCoroutineContext, state: Lifecycle.State = Lifecycle.State.CREATED, ) { - view.repeatWhenAttached { repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } } } + view.repeatWhenAttached(coroutineContext) { + repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java index e7e6918325a7..bdd496ec219b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java @@ -18,6 +18,8 @@ package com.android.systemui.clipboardoverlay; import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_ENABLED; +import static com.google.android.setupcompat.util.WizardManagerHelper.SETTINGS_SECURE_USER_SETUP_COMPLETE; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -32,6 +34,7 @@ import android.content.ClipDescription; import android.content.ClipboardManager; import android.os.PersistableBundle; import android.provider.DeviceConfig; +import android.provider.Settings; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -66,6 +69,8 @@ public class ClipboardListenerTest extends SysuiTestCase { @Mock private ClipboardOverlayController mOverlayController; @Mock + private ClipboardToast mClipboardToast; + @Mock private UiEventLogger mUiEventLogger; @Mock private FeatureFlags mFeatureFlags; @@ -84,6 +89,8 @@ public class ClipboardListenerTest extends SysuiTestCase { @Spy private Provider<ClipboardOverlayController> mOverlayControllerProvider; + private ClipboardListener mClipboardListener; + @Before public void setup() { @@ -93,7 +100,8 @@ public class ClipboardListenerTest extends SysuiTestCase { when(mClipboardOverlayControllerLegacyFactory.create(any())) .thenReturn(mOverlayControllerLegacy); when(mClipboardManager.hasPrimaryClip()).thenReturn(true); - + Settings.Secure.putInt( + mContext.getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 1); mSampleClipData = new ClipData("Test", new String[]{"text/plain"}, new ClipData.Item("Test Item")); @@ -101,16 +109,17 @@ public class ClipboardListenerTest extends SysuiTestCase { when(mClipboardManager.getPrimaryClipSource()).thenReturn(mSampleSource); mDeviceConfigProxy = new DeviceConfigProxyFake(); + + mClipboardListener = new ClipboardListener(getContext(), mDeviceConfigProxy, + mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, + mClipboardToast, mClipboardManager, mUiEventLogger, mFeatureFlags); } @Test public void test_disabled() { mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, "false", false); - ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, - mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, - mClipboardManager, mUiEventLogger, mFeatureFlags); - listener.start(); + mClipboardListener.start(); verifyZeroInteractions(mClipboardManager); verifyZeroInteractions(mUiEventLogger); } @@ -119,10 +128,7 @@ public class ClipboardListenerTest extends SysuiTestCase { public void test_enabled() { mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, "true", false); - ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, - mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, - mClipboardManager, mUiEventLogger, mFeatureFlags); - listener.start(); + mClipboardListener.start(); verify(mClipboardManager).addPrimaryClipChangedListener(any()); verifyZeroInteractions(mUiEventLogger); } @@ -133,11 +139,8 @@ public class ClipboardListenerTest extends SysuiTestCase { mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, "true", false); - ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, - mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, - mClipboardManager, mUiEventLogger, mFeatureFlags); - listener.start(); - listener.onPrimaryClipChanged(); + mClipboardListener.start(); + mClipboardListener.onPrimaryClipChanged(); verify(mClipboardOverlayControllerLegacyFactory).create(any()); @@ -152,14 +155,14 @@ public class ClipboardListenerTest extends SysuiTestCase { // Should clear the overlay controller mRunnableCaptor.getValue().run(); - listener.onPrimaryClipChanged(); + mClipboardListener.onPrimaryClipChanged(); verify(mClipboardOverlayControllerLegacyFactory, times(2)).create(any()); // Not calling the runnable here, just change the clip again and verify that the overlay is // NOT recreated. - listener.onPrimaryClipChanged(); + mClipboardListener.onPrimaryClipChanged(); verify(mClipboardOverlayControllerLegacyFactory, times(2)).create(any()); verifyZeroInteractions(mOverlayControllerProvider); @@ -171,11 +174,8 @@ public class ClipboardListenerTest extends SysuiTestCase { mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, "true", false); - ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, - mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, - mClipboardManager, mUiEventLogger, mFeatureFlags); - listener.start(); - listener.onPrimaryClipChanged(); + mClipboardListener.start(); + mClipboardListener.onPrimaryClipChanged(); verify(mOverlayControllerProvider).get(); @@ -190,14 +190,14 @@ public class ClipboardListenerTest extends SysuiTestCase { // Should clear the overlay controller mRunnableCaptor.getValue().run(); - listener.onPrimaryClipChanged(); + mClipboardListener.onPrimaryClipChanged(); verify(mOverlayControllerProvider, times(2)).get(); // Not calling the runnable here, just change the clip again and verify that the overlay is // NOT recreated. - listener.onPrimaryClipChanged(); + mClipboardListener.onPrimaryClipChanged(); verify(mOverlayControllerProvider, times(2)).get(); verifyZeroInteractions(mClipboardOverlayControllerLegacyFactory); @@ -233,13 +233,10 @@ public class ClipboardListenerTest extends SysuiTestCase { public void test_logging_enterAndReenter() { when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(false); - ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, - mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, - mClipboardManager, mUiEventLogger, mFeatureFlags); - listener.start(); + mClipboardListener.start(); - listener.onPrimaryClipChanged(); - listener.onPrimaryClipChanged(); + mClipboardListener.onPrimaryClipChanged(); + mClipboardListener.onPrimaryClipChanged(); verify(mUiEventLogger, times(1)).log( ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource); @@ -251,17 +248,29 @@ public class ClipboardListenerTest extends SysuiTestCase { public void test_logging_enterAndReenter_new() { when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(true); - ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, - mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, - mClipboardManager, mUiEventLogger, mFeatureFlags); - listener.start(); + mClipboardListener.start(); - listener.onPrimaryClipChanged(); - listener.onPrimaryClipChanged(); + mClipboardListener.onPrimaryClipChanged(); + mClipboardListener.onPrimaryClipChanged(); verify(mUiEventLogger, times(1)).log( ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource); verify(mUiEventLogger, times(1)).log( ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED, 0, mSampleSource); } + + @Test + public void test_userSetupIncomplete_showsToast() { + Settings.Secure.putInt( + mContext.getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0); + + mClipboardListener.start(); + mClipboardListener.onPrimaryClipChanged(); + + verify(mUiEventLogger, times(1)).log( + ClipboardOverlayEvent.CLIPBOARD_TOAST_SHOWN, 0, mSampleSource); + verify(mClipboardToast, times(1)).showCopiedToast(); + verifyZeroInteractions(mOverlayControllerProvider); + verifyZeroInteractions(mClipboardOverlayControllerLegacyFactory); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/demomode/DemoModeControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/demomode/DemoModeControllerTest.kt new file mode 100644 index 000000000000..87c66b5a98ac --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/demomode/DemoModeControllerTest.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.demomode + +import android.content.Intent +import android.os.Bundle +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.demomode.DemoMode.ACTION_DEMO +import com.android.systemui.demomode.DemoMode.COMMAND_STATUS +import com.android.systemui.dump.DumpManager +import com.android.systemui.util.settings.FakeSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +@SmallTest +class DemoModeControllerTest : SysuiTestCase() { + private lateinit var underTest: DemoModeController + + @Mock private lateinit var dumpManager: DumpManager + + private val globalSettings = FakeSettings() + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + @Before + fun setUp() { + allowTestableLooperAsMainThread() + + MockitoAnnotations.initMocks(this) + + globalSettings.putInt(DemoModeController.DEMO_MODE_ALLOWED, 1) + globalSettings.putInt(DemoModeController.DEMO_MODE_ON, 1) + + underTest = + DemoModeController( + context = context, + dumpManager = dumpManager, + globalSettings = globalSettings, + broadcastDispatcher = fakeBroadcastDispatcher + ) + + underTest.initialize() + } + + @Test + fun `demo command flow - returns args`() = + testScope.runTest { + var latest: Bundle? = null + val flow = underTest.demoFlowForCommand(TEST_COMMAND) + val job = launch { flow.collect { latest = it } } + + sendDemoCommand(args = mapOf("key1" to "val1")) + assertThat(latest!!.getString("key1")).isEqualTo("val1") + + sendDemoCommand(args = mapOf("key2" to "val2")) + assertThat(latest!!.getString("key2")).isEqualTo("val2") + + job.cancel() + } + + private fun sendDemoCommand(command: String? = TEST_COMMAND, args: Map<String, String>) { + val intent = Intent(ACTION_DEMO) + intent.putExtra("command", command) + args.forEach { arg -> intent.putExtra(arg.key, arg.value) } + + fakeBroadcastDispatcher.registeredReceivers.forEach { it.onReceive(context, intent) } + } + + companion object { + // Use a valid command until we properly fake out everything + const val TEST_COMMAND = COMMAND_STATUS + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamCallbackControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt index 003efbfdc4d7..9f534ef14f54 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamCallbackControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.dreams import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -30,23 +31,27 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidTestingRunner::class) -class DreamCallbackControllerTest : SysuiTestCase() { +class DreamOverlayCallbackControllerTest : SysuiTestCase() { - @Mock private lateinit var callback: DreamCallbackController.DreamCallback + @Mock private lateinit var callback: DreamOverlayCallbackController.Callback - private lateinit var underTest: DreamCallbackController + private lateinit var underTest: DreamOverlayCallbackController @Before fun setUp() { MockitoAnnotations.initMocks(this) - underTest = DreamCallbackController() + underTest = DreamOverlayCallbackController() } @Test - fun testOnWakeUpInvokesCallback() { + fun onWakeUpInvokesCallback() { + underTest.onStartDream() + assertThat(underTest.isDreaming).isEqualTo(true) + underTest.addCallback(callback) underTest.onWakeUp() verify(callback).onWakeUp() + assertThat(underTest.isDreaming).isEqualTo(false) // Adding twice should not invoke twice reset(callback) @@ -60,4 +65,27 @@ class DreamCallbackControllerTest : SysuiTestCase() { underTest.onWakeUp() verify(callback, never()).onWakeUp() } + + @Test + fun onStartDreamInvokesCallback() { + underTest.addCallback(callback) + + assertThat(underTest.isDreaming).isEqualTo(false) + + underTest.onStartDream() + verify(callback).onStartDream() + assertThat(underTest.isDreaming).isEqualTo(true) + + // Adding twice should not invoke twice + reset(callback) + underTest.addCallback(callback) + underTest.onStartDream() + verify(callback, times(1)).onStartDream() + + // After remove, no call to callback + reset(callback) + underTest.removeCallback(callback) + underTest.onStartDream() + verify(callback, never()).onStartDream() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java index d6f8deabc270..4568d1e9b3bd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java @@ -114,7 +114,7 @@ public class DreamOverlayServiceTest extends SysuiTestCase { UiEventLogger mUiEventLogger; @Mock - DreamCallbackController mDreamCallbackController; + DreamOverlayCallbackController mDreamOverlayCallbackController; @Captor ArgumentCaptor<View> mViewCaptor; @@ -145,7 +145,7 @@ public class DreamOverlayServiceTest extends SysuiTestCase { mKeyguardUpdateMonitor, mUiEventLogger, LOW_LIGHT_COMPONENT, - mDreamCallbackController); + mDreamOverlayCallbackController); } @Test @@ -357,7 +357,7 @@ public class DreamOverlayServiceTest extends SysuiTestCase { mService.onWakeUp(callback); mMainExecutor.runAllReady(); verify(mDreamOverlayContainerViewController).wakeUp(callback, mMainExecutor); - verify(mDreamCallbackController).onWakeUp(); + verify(mDreamOverlayCallbackController).onWakeUp(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt index 563d44d3f15f..be712f699b7b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -28,8 +28,7 @@ import com.android.systemui.doze.DozeHost import com.android.systemui.doze.DozeMachine import com.android.systemui.doze.DozeTransitionCallback import com.android.systemui.doze.DozeTransitionListener -import com.android.systemui.dreams.DreamCallbackController -import com.android.systemui.dreams.DreamCallbackController.DreamCallback +import com.android.systemui.dreams.DreamOverlayCallbackController import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.BiometricUnlockSource @@ -68,7 +67,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var dozeTransitionListener: DozeTransitionListener @Mock private lateinit var authController: AuthController @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor - @Mock private lateinit var dreamCallbackController: DreamCallbackController + @Mock private lateinit var dreamOverlayCallbackController: DreamOverlayCallbackController private lateinit var underTest: KeyguardRepositoryImpl @@ -86,7 +85,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { keyguardUpdateMonitor, dozeTransitionListener, authController, - dreamCallbackController, + dreamOverlayCallbackController, ) } @@ -171,6 +170,29 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { } @Test + fun isKeyguardOccluded() = + runTest(UnconfinedTestDispatcher()) { + whenever(keyguardStateController.isOccluded).thenReturn(false) + var latest: Boolean? = null + val job = underTest.isKeyguardOccluded.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + val captor = argumentCaptor<KeyguardStateController.Callback>() + verify(keyguardStateController).addCallback(captor.capture()) + + whenever(keyguardStateController.isOccluded).thenReturn(true) + captor.value.onKeyguardShowingChanged() + assertThat(latest).isTrue() + + whenever(keyguardStateController.isOccluded).thenReturn(false) + captor.value.onKeyguardShowingChanged() + assertThat(latest).isFalse() + + job.cancel() + } + + @Test fun isDozing() = runTest(UnconfinedTestDispatcher()) { var latest: Boolean? = null @@ -343,19 +365,22 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { } @Test - fun isDreamingFromDreamCallbackController() = + fun isDreamingFromDreamOverlayCallbackController() = runTest(UnconfinedTestDispatcher()) { - whenever(keyguardUpdateMonitor.isDreaming()).thenReturn(true) + whenever(dreamOverlayCallbackController.isDreaming).thenReturn(false) var latest: Boolean? = null - val job = underTest.isDreaming.onEach { latest = it }.launchIn(this) + val job = underTest.isDreamingWithOverlay.onEach { latest = it }.launchIn(this) - assertThat(latest).isTrue() + assertThat(latest).isFalse() val listener = - withArgCaptor<DreamCallbackController.DreamCallback> { - verify(dreamCallbackController).addCallback(capture()) + withArgCaptor<DreamOverlayCallbackController.Callback> { + verify(dreamOverlayCallbackController).addCallback(capture()) } + listener.onStartDream() + assertThat(latest).isTrue() + listener.onWakeUp() assertThat(latest).isFalse() 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 a6cf84053861..d2b7838274a9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt @@ -23,6 +23,8 @@ import com.android.systemui.animation.Interpolators import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepositoryImpl +import com.android.systemui.keyguard.shared.model.DozeStateModel +import com.android.systemui.keyguard.shared.model.DozeTransitionModel import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.keyguard.shared.model.WakeSleepReason @@ -42,6 +44,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 import org.mockito.Mock +import org.mockito.Mockito.reset import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -64,8 +67,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { // Used to verify transition requests for test output @Mock private lateinit var mockTransitionRepository: KeyguardTransitionRepository - private lateinit var lockscreenBouncerTransitionInteractor: - LockscreenBouncerTransitionInteractor + private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor + private lateinit var fromDreamingTransitionInteractor: FromDreamingTransitionInteractor @Before fun setUp() { @@ -79,25 +82,82 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { transitionRepository = KeyguardTransitionRepositoryImpl() runner = KeyguardTransitionRunner(transitionRepository) - lockscreenBouncerTransitionInteractor = - LockscreenBouncerTransitionInteractor( + fromLockscreenTransitionInteractor = + FromLockscreenTransitionInteractor( scope = testScope, keyguardInteractor = KeyguardInteractor(keyguardRepository), shadeRepository = shadeRepository, keyguardTransitionRepository = mockTransitionRepository, keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), ) - lockscreenBouncerTransitionInteractor.start() + fromLockscreenTransitionInteractor.start() + + fromDreamingTransitionInteractor = + FromDreamingTransitionInteractor( + scope = testScope, + keyguardInteractor = KeyguardInteractor(keyguardRepository), + keyguardTransitionRepository = mockTransitionRepository, + keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), + ) + fromDreamingTransitionInteractor.start() } @Test + fun `DREAMING to LOCKSCREEN`() = + testScope.runTest { + // GIVEN a device is dreaming and occluded + keyguardRepository.setDreamingWithOverlay(true) + keyguardRepository.setKeyguardOccluded(true) + runCurrent() + + // GIVEN a prior transition has run to DREAMING + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.DREAMING, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN doze is complete + keyguardRepository.setDozeTransitionModel( + DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) + ) + // AND dreaming has stopped + keyguardRepository.setDreamingWithOverlay(false) + // AND occluded has stopped + keyguardRepository.setKeyguardOccluded(false) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to BOUNCER should occur + assertThat(info.ownerName).isEqualTo("FromDreamingTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.DREAMING) + assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test fun `LOCKSCREEN to BOUNCER via bouncer showing call`() = testScope.runTest { // GIVEN a device that has at least woken up keyguardRepository.setWakefulnessModel(startingToWake()) runCurrent() - // GIVEN a transition has run to LOCKSCREEN + // GIVEN a prior transition has run to LOCKSCREEN runner.startTransition( testScope, TransitionInfo( @@ -122,7 +182,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { verify(mockTransitionRepository).startTransition(capture()) } // THEN a transition to BOUNCER should occur - assertThat(info.ownerName).isEqualTo("LockscreenBouncerTransitionInteractor") + assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor") assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN) assertThat(info.to).isEqualTo(KeyguardState.BOUNCER) assertThat(info.animator).isNotNull() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt index c54e456416c7..557166301d64 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt @@ -21,7 +21,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository -import com.android.systemui.keyguard.domain.interactor.DreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION +import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.AnimationParams import com.android.systemui.keyguard.shared.model.KeyguardState diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt new file mode 100644 index 000000000000..98d292d689e4 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.AnimationParams +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel.Companion.LOCKSCREEN_ALPHA +import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class OccludedToLockscreenTransitionViewModelTest : SysuiTestCase() { + private lateinit var underTest: OccludedToLockscreenTransitionViewModel + private lateinit var repository: FakeKeyguardTransitionRepository + + @Before + fun setUp() { + repository = FakeKeyguardTransitionRepository() + val interactor = KeyguardTransitionInteractor(repository) + underTest = OccludedToLockscreenTransitionViewModel(interactor) + } + + @Test + fun lockscreenFadeIn() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf<Float>() + + val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this) + + repository.sendTransitionStep(step(0f)) + // Should start running here... + repository.sendTransitionStep(step(0.3f)) + repository.sendTransitionStep(step(0.4f)) + repository.sendTransitionStep(step(0.5f)) + // ...up to here + repository.sendTransitionStep(step(0.6f)) + repository.sendTransitionStep(step(1f)) + + // Only two values should be present, since the dream overlay runs for a small fraction + // of the overall animation time + assertThat(values.size).isEqualTo(3) + assertThat(values[0]).isEqualTo(animValue(0.3f, LOCKSCREEN_ALPHA)) + assertThat(values[1]).isEqualTo(animValue(0.4f, LOCKSCREEN_ALPHA)) + assertThat(values[2]).isEqualTo(animValue(0.5f, LOCKSCREEN_ALPHA)) + + job.cancel() + } + + @Test + fun lockscreenTranslationY() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf<Float>() + + val pixels = 100 + val job = + underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this) + + repository.sendTransitionStep(step(0f)) + repository.sendTransitionStep(step(0.3f)) + repository.sendTransitionStep(step(0.5f)) + repository.sendTransitionStep(step(1f)) + + assertThat(values.size).isEqualTo(4) + assertThat(values[0]) + .isEqualTo( + -pixels + + EMPHASIZED_DECELERATE.getInterpolation( + animValue(0f, LOCKSCREEN_TRANSLATION_Y) + ) * pixels + ) + assertThat(values[1]) + .isEqualTo( + -pixels + + EMPHASIZED_DECELERATE.getInterpolation( + animValue(0.3f, LOCKSCREEN_TRANSLATION_Y) + ) * pixels + ) + assertThat(values[2]) + .isEqualTo( + -pixels + + EMPHASIZED_DECELERATE.getInterpolation( + animValue(0.5f, LOCKSCREEN_TRANSLATION_Y) + ) * pixels + ) + assertThat(values[3]) + .isEqualTo( + -pixels + + EMPHASIZED_DECELERATE.getInterpolation( + animValue(1f, LOCKSCREEN_TRANSLATION_Y) + ) * pixels + ) + + job.cancel() + } + + private fun animValue(stepValue: Float, params: AnimationParams): Float { + val totalDuration = TO_LOCKSCREEN_DURATION + val startValue = (params.startTime / totalDuration).toFloat() + + val multiplier = (totalDuration / params.duration).toFloat() + return (stepValue - startValue) * multiplier + } + + private fun step(value: Float): TransitionStep { + return TransitionStep( + from = KeyguardState.OCCLUDED, + to = KeyguardState.LOCKSCREEN, + value = value, + transitionState = TransitionState.RUNNING, + ownerName = "OccludedToLockscreenTransitionViewModelTest" + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index ad993c391bcd..65b2ac0eb2d3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -107,6 +107,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInterac import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel; +import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel; import com.android.systemui.media.controls.pipeline.MediaDataManager; import com.android.systemui.media.controls.ui.KeyguardMediaController; import com.android.systemui.media.controls.ui.MediaHierarchyManager; @@ -188,6 +189,8 @@ import org.mockito.stubbing.Answer; import java.util.List; import java.util.Optional; +import kotlinx.coroutines.CoroutineDispatcher; + @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper @@ -289,7 +292,9 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Mock private KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel; @Mock private KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor; @Mock private DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel; + @Mock private OccludedToLockscreenTransitionViewModel mOccludedToLockscreenTransitionViewModel; @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor; + @Mock private CoroutineDispatcher mMainDispatcher; @Mock private MotionEvent mDownMotionEvent; @Captor private ArgumentCaptor<NotificationStackScrollLayout.OnEmptySpaceClickListener> @@ -507,6 +512,8 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { mKeyguardBottomAreaViewModel, mKeyguardBottomAreaInteractor, mDreamingToLockscreenTransitionViewModel, + mOccludedToLockscreenTransitionViewModel, + mMainDispatcher, mKeyguardTransitionInteractor, mDumpManager); mNotificationPanelViewController.initDependencies( diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt index 43c694245eba..3e769e94b6ad 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt @@ -21,6 +21,7 @@ import android.provider.Settings.Secure.DOZE_DOUBLE_TAP_GESTURE import android.provider.Settings.Secure.DOZE_TAP_SCREEN_GESTURE import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper +import android.os.PowerManager import android.view.MotionEvent import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -36,9 +37,9 @@ import org.junit.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.ArgumentMatchers.anyLong -import org.mockito.ArgumentMatchers.anyObject import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito.never @@ -106,7 +107,8 @@ class PulsingGestureListenerTest : SysuiTestCase() { underTest.onSingleTapUp(upEv) // THEN wake up device if dozing - verify(centralSurfaces).wakeUpIfDozing(anyLong(), anyObject(), anyString()) + verify(centralSurfaces).wakeUpIfDozing( + anyLong(), any(), anyString(), eq(PowerManager.WAKE_REASON_TAP)) } @Test @@ -125,7 +127,8 @@ class PulsingGestureListenerTest : SysuiTestCase() { underTest.onDoubleTapEvent(upEv) // THEN wake up device if dozing - verify(centralSurfaces).wakeUpIfDozing(anyLong(), anyObject(), anyString()) + verify(centralSurfaces).wakeUpIfDozing( + anyLong(), any(), anyString(), eq(PowerManager.WAKE_REASON_TAP)) } @Test @@ -156,7 +159,8 @@ class PulsingGestureListenerTest : SysuiTestCase() { underTest.onSingleTapUp(upEv) // THEN the device doesn't wake up - verify(centralSurfaces, never()).wakeUpIfDozing(anyLong(), anyObject(), anyString()) + verify(centralSurfaces, never()).wakeUpIfDozing( + anyLong(), any(), anyString(), anyInt()) } @Test @@ -203,7 +207,8 @@ class PulsingGestureListenerTest : SysuiTestCase() { underTest.onDoubleTapEvent(upEv) // THEN the device doesn't wake up - verify(centralSurfaces, never()).wakeUpIfDozing(anyLong(), anyObject(), anyString()) + verify(centralSurfaces, never()).wakeUpIfDozing( + anyLong(), any(), anyString(), anyInt()) } @Test @@ -222,7 +227,8 @@ class PulsingGestureListenerTest : SysuiTestCase() { underTest.onSingleTapUp(upEv) // THEN the device doesn't wake up - verify(centralSurfaces, never()).wakeUpIfDozing(anyLong(), anyObject(), anyString()) + verify(centralSurfaces, never()).wakeUpIfDozing( + anyLong(), any(), anyString(), anyInt()) } @Test @@ -241,7 +247,8 @@ class PulsingGestureListenerTest : SysuiTestCase() { underTest.onDoubleTapEvent(upEv) // THEN the device doesn't wake up - verify(centralSurfaces, never()).wakeUpIfDozing(anyLong(), anyObject(), anyString()) + verify(centralSurfaces, never()).wakeUpIfDozing( + anyLong(), any(), anyString(), anyInt()) } fun updateSettings() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index ae60c73b89b9..09254ad0faf2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -31,6 +31,7 @@ import static junit.framework.TestCase.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; @@ -176,6 +177,8 @@ import com.android.systemui.volume.VolumeComponent; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.startingsurface.StartingSurface; +import dagger.Lazy; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -188,8 +191,6 @@ import java.io.ByteArrayOutputStream; import java.io.PrintWriter; import java.util.Optional; -import dagger.Lazy; - @SmallTest @RunWith(AndroidTestingRunner.class) @RunWithLooper(setAsMainLooper = true) @@ -304,6 +305,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Mock private ViewRootImpl mViewRootImpl; @Mock private WindowOnBackInvokedDispatcher mOnBackInvokedDispatcher; @Captor private ArgumentCaptor<OnBackInvokedCallback> mOnBackInvokedCallback; + @Mock IPowerManager mPowerManagerService; private ShadeController mShadeController; private final FakeSystemClock mFakeSystemClock = new FakeSystemClock(); @@ -317,9 +319,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { public void setup() throws Exception { MockitoAnnotations.initMocks(this); - IPowerManager powerManagerService = mock(IPowerManager.class); IThermalService thermalService = mock(IThermalService.class); - mPowerManager = new PowerManager(mContext, powerManagerService, thermalService, + mPowerManager = new PowerManager(mContext, mPowerManagerService, thermalService, Handler.createAsync(Looper.myLooper())); mNotificationInterruptStateProvider = @@ -361,7 +362,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { when(mStackScrollerController.getView()).thenReturn(mStackScroller); when(mStackScroller.generateLayoutParams(any())).thenReturn(new LayoutParams(0, 0)); when(mNotificationPanelView.getLayoutParams()).thenReturn(new LayoutParams(0, 0)); - when(powerManagerService.isInteractive()).thenReturn(true); + when(mPowerManagerService.isInteractive()).thenReturn(true); when(mStackScroller.getActivatedChild()).thenReturn(null); doAnswer(invocation -> { @@ -1186,6 +1187,34 @@ public class CentralSurfacesImplTest extends SysuiTestCase { verify(mStatusBarStateController).setState(SHADE); } + @Test + public void dozing_wakeUp() throws RemoteException { + // GIVEN can wakeup when dozing & is dozing + when(mScreenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true); + setDozing(true); + + // WHEN wakeup is requested + final int wakeReason = PowerManager.WAKE_REASON_TAP; + mCentralSurfaces.wakeUpIfDozing(0, null, "", wakeReason); + + // THEN power manager receives wakeup + verify(mPowerManagerService).wakeUp(eq(0L), eq(wakeReason), anyString(), anyString()); + } + + @Test + public void notDozing_noWakeUp() throws RemoteException { + // GIVEN can wakeup when dozing and NOT dozing + when(mScreenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true); + setDozing(false); + + // WHEN wakeup is requested + final int wakeReason = PowerManager.WAKE_REASON_TAP; + mCentralSurfaces.wakeUpIfDozing(0, null, "", wakeReason); + + // THEN power manager receives wakeup + verify(mPowerManagerService, never()).wakeUp(anyLong(), anyInt(), anyString(), anyString()); + } + /** * Configures the appropriate mocks and then calls {@link CentralSurfacesImpl#updateIsKeyguard} * to reconfigure the keyguard to reflect the requested showing/occluded states. @@ -1222,6 +1251,13 @@ public class CentralSurfacesImplTest extends SysuiTestCase { states); } + private void setDozing(boolean isDozing) { + ArgumentCaptor<StatusBarStateController.StateListener> callbackCaptor = + ArgumentCaptor.forClass(StatusBarStateController.StateListener.class); + verify(mStatusBarStateController).addCallback(callbackCaptor.capture(), anyInt()); + callbackCaptor.getValue().onDozingChanged(isDozing); + } + public static class TestableNotificationInterruptStateProviderImpl extends NotificationInterruptStateProviderImpl { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt new file mode 100644 index 000000000000..f822ba0f0a62 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.data.model + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.log.table.TableRowLogger +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_ACTIVITY_DIRECTION +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_CARRIER_NETWORK_CHANGE +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_CDMA_LEVEL +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_CONNECTION_STATE +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_EMERGENCY +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_IS_GSM +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_OPERATOR +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_PRIMARY_LEVEL +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_RESOLVED_NETWORK_TYPE +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_ROAMING +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +@SmallTest +class MobileConnectionModelTest : SysuiTestCase() { + + @Test + fun `log diff - initial log contains all columns`() { + val logger = TestLogger() + val connection = MobileConnectionModel() + + connection.logFull(logger) + + assertThat(logger.changes) + .contains(Pair(COL_EMERGENCY, connection.isEmergencyOnly.toString())) + assertThat(logger.changes).contains(Pair(COL_ROAMING, connection.isRoaming.toString())) + assertThat(logger.changes) + .contains(Pair(COL_OPERATOR, connection.operatorAlphaShort.toString())) + assertThat(logger.changes).contains(Pair(COL_IS_GSM, connection.isGsm.toString())) + assertThat(logger.changes).contains(Pair(COL_CDMA_LEVEL, connection.cdmaLevel.toString())) + assertThat(logger.changes) + .contains(Pair(COL_PRIMARY_LEVEL, connection.primaryLevel.toString())) + assertThat(logger.changes) + .contains(Pair(COL_CONNECTION_STATE, connection.dataConnectionState.toString())) + assertThat(logger.changes) + .contains(Pair(COL_ACTIVITY_DIRECTION, connection.dataActivityDirection.toString())) + assertThat(logger.changes) + .contains( + Pair(COL_CARRIER_NETWORK_CHANGE, connection.carrierNetworkChangeActive.toString()) + ) + assertThat(logger.changes) + .contains(Pair(COL_RESOLVED_NETWORK_TYPE, connection.resolvedNetworkType.toString())) + } + + @Test + fun `log diff - primary level changes - only level is logged`() { + val logger = TestLogger() + val connectionOld = MobileConnectionModel(primaryLevel = 1) + + val connectionNew = MobileConnectionModel(primaryLevel = 2) + + connectionNew.logDiffs(connectionOld, logger) + + assertThat(logger.changes).isEqualTo(listOf(Pair(COL_PRIMARY_LEVEL, "2"))) + } + + private class TestLogger : TableRowLogger { + val changes = mutableListOf<Pair<String, String>>() + + override fun logChange(columnName: String, value: String?) { + changes.add(Pair(columnName, value.toString())) + } + + override fun logChange(columnName: String, value: Int) { + changes.add(Pair(columnName, value.toString())) + } + + override fun logChange(columnName: String, value: Boolean) { + changes.add(Pair(columnName, value.toString())) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt index 59eec5327c12..d6a9ee325b2e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt @@ -16,12 +16,16 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import kotlinx.coroutines.flow.MutableStateFlow // TODO(b/261632894): remove this in favor of the real impl or DemoMobileConnectionRepository -class FakeMobileConnectionRepository(override val subId: Int) : MobileConnectionRepository { +class FakeMobileConnectionRepository( + override val subId: Int, + override val tableLogBuffer: TableLogBuffer, +) : MobileConnectionRepository { private val _connectionInfo = MutableStateFlow(MobileConnectionModel()) override val connectionInfo = _connectionInfo diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt index 04d3cdd89ab7..7f93328ee95e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt @@ -22,14 +22,17 @@ import android.telephony.TelephonyManager import com.android.settingslib.SignalIcon import com.android.settingslib.mobile.MobileMappings import com.android.settingslib.mobile.TelephonyIcons +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import kotlinx.coroutines.flow.MutableStateFlow // TODO(b/261632894): remove this in favor of the real impl or DemoMobileConnectionsRepository -class FakeMobileConnectionsRepository(mobileMappings: MobileMappingsProxy) : - MobileConnectionsRepository { +class FakeMobileConnectionsRepository( + mobileMappings: MobileMappingsProxy, + val tableLogBuffer: TableLogBuffer, +) : MobileConnectionsRepository { val GSM_KEY = mobileMappings.toIconKey(GSM) val LTE_KEY = mobileMappings.toIconKey(LTE) val UMTS_KEY = mobileMappings.toIconKey(UMTS) @@ -63,7 +66,7 @@ class FakeMobileConnectionsRepository(mobileMappings: MobileMappingsProxy) : private val subIdRepos = mutableMapOf<Int, MobileConnectionRepository>() override fun getRepoForSubId(subId: Int): MobileConnectionRepository { return subIdRepos[subId] - ?: FakeMobileConnectionRepository(subId).also { subIdRepos[subId] = it } + ?: FakeMobileConnectionRepository(subId, tableLogBuffer).also { subIdRepos[subId] = it } } private val _globalMobileDataSettingChangedEvent = MutableStateFlow(Unit) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt index 18ae90db881a..5d377a8658a5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt @@ -24,6 +24,8 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.demomode.DemoMode import com.android.systemui.demomode.DemoModeController +import com.android.systemui.dump.DumpManager +import com.android.systemui.log.table.TableLogBufferFactory import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoModeMobileConnectionDataSource @@ -37,6 +39,7 @@ import com.android.systemui.util.mockito.kotlinArgumentCaptor import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -69,12 +72,14 @@ class MobileRepositorySwitcherTest : SysuiTestCase() { private lateinit var realRepo: MobileConnectionsRepositoryImpl private lateinit var demoRepo: DemoMobileConnectionsRepository private lateinit var mockDataSource: DemoModeMobileConnectionDataSource + private lateinit var logFactory: TableLogBufferFactory @Mock private lateinit var connectivityManager: ConnectivityManager @Mock private lateinit var subscriptionManager: SubscriptionManager @Mock private lateinit var telephonyManager: TelephonyManager @Mock private lateinit var logger: ConnectivityPipelineLogger @Mock private lateinit var demoModeController: DemoModeController + @Mock private lateinit var dumpManager: DumpManager private val globalSettings = FakeSettings() private val fakeNetworkEventsFlow = MutableStateFlow<FakeNetworkEventModel?>(null) @@ -86,6 +91,8 @@ class MobileRepositorySwitcherTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) + logFactory = TableLogBufferFactory(dumpManager, FakeSystemClock()) + // Never start in demo mode whenever(demoModeController.isInDemoMode).thenReturn(false) @@ -114,6 +121,7 @@ class MobileRepositorySwitcherTest : SysuiTestCase() { dataSource = mockDataSource, scope = scope, context = context, + logFactory = logFactory, ) underTest = diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt index 3d5316d1f19d..210208532dd4 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 @@ -23,6 +23,7 @@ import androidx.test.filters.SmallTest import com.android.settingslib.SignalIcon import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.SysuiTestCase +import com.android.systemui.log.table.TableLogBufferFactory import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel @@ -30,6 +31,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancel @@ -54,6 +56,9 @@ import org.junit.runners.Parameterized.Parameters @RunWith(Parameterized::class) internal class DemoMobileConnectionParameterizedTest(private val testCase: TestCase) : SysuiTestCase() { + + private val logFactory = TableLogBufferFactory(mock(), FakeSystemClock()) + private val testDispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(testDispatcher) @@ -76,6 +81,7 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC dataSource = mockDataSource, scope = testScope.backgroundScope, context = context, + logFactory = logFactory, ) connectionsRepo.startProcessingCommands() 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 34f30eb7c0a6..cdbe75e855bc 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 @@ -23,6 +23,8 @@ import androidx.test.filters.SmallTest import com.android.settingslib.SignalIcon import com.android.settingslib.mobile.TelephonyIcons.THREE_G import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.DumpManager +import com.android.systemui.log.table.TableLogBufferFactory import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel @@ -32,6 +34,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import junit.framework.Assert import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -47,6 +50,9 @@ import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) @SmallTest class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { + private val dumpManager: DumpManager = mock() + private val logFactory = TableLogBufferFactory(dumpManager, FakeSystemClock()) + private val testDispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(testDispatcher) @@ -68,6 +74,7 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { dataSource = mockDataSource, scope = testScope.backgroundScope, context = context, + logFactory = logFactory, ) underTest.startProcessingCommands() 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 7fa80653f29c..7970443f69b1 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,6 +50,7 @@ import android.telephony.TelephonyManager.NETWORK_TYPE_LTE import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel @@ -87,14 +88,15 @@ import org.mockito.MockitoAnnotations @SmallTest class MobileConnectionRepositoryTest : SysuiTestCase() { private lateinit var underTest: MobileConnectionRepositoryImpl + private lateinit var connectionsRepo: FakeMobileConnectionsRepository @Mock private lateinit var telephonyManager: TelephonyManager @Mock private lateinit var logger: ConnectivityPipelineLogger + @Mock private lateinit var tableLogger: TableLogBuffer private val scope = CoroutineScope(IMMEDIATE) private val mobileMappings = FakeMobileMappingsProxy() private val globalSettings = FakeSettings() - private val connectionsRepo = FakeMobileConnectionsRepository(mobileMappings) @Before fun setUp() { @@ -102,6 +104,8 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { globalSettings.userId = UserHandle.USER_ALL whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID) + connectionsRepo = FakeMobileConnectionsRepository(mobileMappings, tableLogger) + underTest = MobileConnectionRepositoryImpl( context, @@ -116,6 +120,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { mobileMappings, IMMEDIATE, logger, + tableLogger, scope, ) } 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 3cc1e8b74668..b8cd7a4f6e0a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt @@ -34,12 +34,15 @@ import com.android.internal.telephony.PhoneConstants import com.android.settingslib.R import com.android.settingslib.mobile.MobileMappings import com.android.systemui.SysuiTestCase +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.TableLogBufferFactory import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.FakeSettings @@ -57,6 +60,7 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -72,6 +76,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { @Mock private lateinit var subscriptionManager: SubscriptionManager @Mock private lateinit var telephonyManager: TelephonyManager @Mock private lateinit var logger: ConnectivityPipelineLogger + @Mock private lateinit var logBufferFactory: TableLogBufferFactory private val mobileMappings = FakeMobileMappingsProxy() @@ -89,6 +94,10 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } } + whenever(logBufferFactory.create(anyString(), anyInt())).thenAnswer { _ -> + mock<TableLogBuffer>() + } + connectionFactory = MobileConnectionRepositoryImpl.Factory( fakeBroadcastDispatcher, @@ -99,6 +108,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { logger = logger, mobileMappingsProxy = mobileMappings, scope = scope, + logFactory = logBufferFactory, ) underTest = @@ -271,6 +281,32 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } @Test + fun `connection repository - log buffer contains sub id in its name`() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptions.launchIn(this) + + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_2)) + getSubscriptionCallback().onSubscriptionsChanged() + + // Get repos to trigger creation + underTest.getRepoForSubId(SUB_1_ID) + verify(logBufferFactory) + .create( + eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_1_ID)), + anyInt(), + ) + underTest.getRepoForSubId(SUB_2_ID) + verify(logBufferFactory) + .create( + eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_2_ID)), + anyInt(), + ) + + job.cancel() + } + + @Test fun testDefaultDataSubId_updatesOnBroadcast() = runBlocking(IMMEDIATE) { var latest: Int? = null diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt index c3519b7c8176..c49458909c78 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt @@ -19,11 +19,14 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.telephony.CellSignalStrength import com.android.settingslib.SignalIcon import com.android.settingslib.mobile.TelephonyIcons +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import kotlinx.coroutines.flow.MutableStateFlow -class FakeMobileIconInteractor : MobileIconInteractor { +class FakeMobileIconInteractor( + override val tableLogBuffer: TableLogBuffer, +) : MobileIconInteractor { override val alwaysShowDataRatIcon = MutableStateFlow(false) override val activity = diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt index 9f300e9e0cf3..19e5516b58a2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt @@ -22,12 +22,15 @@ import android.telephony.TelephonyManager.NETWORK_TYPE_LTE import android.telephony.TelephonyManager.NETWORK_TYPE_UMTS import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.TelephonyIcons +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -class FakeMobileIconsInteractor(mobileMappings: MobileMappingsProxy) : MobileIconsInteractor { +class FakeMobileIconsInteractor( + mobileMappings: MobileMappingsProxy, + val tableLogBuffer: TableLogBuffer, +) : MobileIconsInteractor { val THREE_G_KEY = mobileMappings.toIconKey(THREE_G) val LTE_KEY = mobileMappings.toIconKey(LTE) val FOUR_G_KEY = mobileMappings.toIconKey(FOUR_G) @@ -48,8 +51,7 @@ class FakeMobileIconsInteractor(mobileMappings: MobileMappingsProxy) : MobileIco override val isDefaultConnectionFailed = MutableStateFlow(false) - private val _filteredSubscriptions = MutableStateFlow<List<SubscriptionModel>>(listOf()) - override val filteredSubscriptions: Flow<List<SubscriptionModel>> = _filteredSubscriptions + override val filteredSubscriptions = MutableStateFlow<List<SubscriptionModel>>(listOf()) private val _activeDataConnectionHasDataEnabled = MutableStateFlow(false) override val activeDataConnectionHasDataEnabled = _activeDataConnectionHasDataEnabled @@ -67,7 +69,7 @@ class FakeMobileIconsInteractor(mobileMappings: MobileMappingsProxy) : MobileIco /** Always returns a new fake interactor */ override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor { - return FakeMobileIconInteractor() + return FakeMobileIconInteractor(tableLogBuffer) } companion object { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt index 4dca780425e5..83c5055a6eda 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 @@ -49,8 +49,8 @@ import org.junit.Test class MobileIconInteractorTest : SysuiTestCase() { private lateinit var underTest: MobileIconInteractor private val mobileMappingsProxy = FakeMobileMappingsProxy() - private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy) - private val connectionRepository = FakeMobileConnectionRepository(SUB_1_ID) + private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy, mock()) + private val connectionRepository = FakeMobileConnectionRepository(SUB_1_ID, mock()) private val scope = CoroutineScope(IMMEDIATE) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt index 85578942ba86..2fa3467587cc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt @@ -20,6 +20,7 @@ import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import androidx.test.filters.SmallTest import com.android.settingslib.mobile.MobileMappings import com.android.systemui.SysuiTestCase +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository @@ -28,6 +29,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSe import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.util.CarrierConfigTracker import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -44,9 +46,9 @@ import org.mockito.MockitoAnnotations @SmallTest class MobileIconsInteractorTest : SysuiTestCase() { private lateinit var underTest: MobileIconsInteractor + private lateinit var connectionsRepository: FakeMobileConnectionsRepository private val userSetupRepository = FakeUserSetupRepository() private val mobileMappingsProxy = FakeMobileMappingsProxy() - private val connectionsRepository = FakeMobileConnectionsRepository(mobileMappingsProxy) private val scope = CoroutineScope(IMMEDIATE) @Mock private lateinit var carrierConfigTracker: CarrierConfigTracker @@ -55,6 +57,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) + connectionsRepository = FakeMobileConnectionsRepository(mobileMappingsProxy, tableLogBuffer) connectionsRepository.setMobileConnectionRepositoryMap( mapOf( SUB_1_ID to CONNECTION_1, @@ -290,21 +293,23 @@ class MobileIconsInteractorTest : SysuiTestCase() { companion object { private val IMMEDIATE = Dispatchers.Main.immediate + private val tableLogBuffer = + TableLogBuffer(8, "MobileIconsInteractorTest", FakeSystemClock()) private const val SUB_1_ID = 1 private val SUB_1 = SubscriptionModel(subscriptionId = SUB_1_ID) - private val CONNECTION_1 = FakeMobileConnectionRepository(SUB_1_ID) + private val CONNECTION_1 = FakeMobileConnectionRepository(SUB_1_ID, tableLogBuffer) private const val SUB_2_ID = 2 private val SUB_2 = SubscriptionModel(subscriptionId = SUB_2_ID) - private val CONNECTION_2 = FakeMobileConnectionRepository(SUB_2_ID) + private val CONNECTION_2 = FakeMobileConnectionRepository(SUB_2_ID, tableLogBuffer) private const val SUB_3_ID = 3 private val SUB_3_OPP = SubscriptionModel(subscriptionId = SUB_3_ID, isOpportunistic = true) - private val CONNECTION_3 = FakeMobileConnectionRepository(SUB_3_ID) + private val CONNECTION_3 = FakeMobileConnectionRepository(SUB_3_ID, tableLogBuffer) private const val SUB_4_ID = 4 private val SUB_4_OPP = SubscriptionModel(subscriptionId = SUB_4_ID, isOpportunistic = true) - private val CONNECTION_4 = FakeMobileConnectionRepository(SUB_4_ID) + private val CONNECTION_4 = FakeMobileConnectionRepository(SUB_4_ID, tableLogBuffer) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt new file mode 100644 index 000000000000..043d55a73076 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.settingslib.mobile.TelephonyIcons +import com.android.systemui.SysuiTestCase +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModelTest.Companion.defaultSignal +import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class LocationBasedMobileIconViewModelTest : SysuiTestCase() { + private lateinit var commonImpl: MobileIconViewModelCommon + private lateinit var homeIcon: HomeMobileIconViewModel + private lateinit var qsIcon: QsMobileIconViewModel + private lateinit var keyguardIcon: KeyguardMobileIconViewModel + private lateinit var interactor: FakeMobileIconInteractor + @Mock private lateinit var logger: ConnectivityPipelineLogger + @Mock private lateinit var constants: ConnectivityConstants + @Mock private lateinit var tableLogBuffer: TableLogBuffer + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + interactor = FakeMobileIconInteractor(tableLogBuffer) + interactor.apply { + setLevel(1) + setIsDefaultDataEnabled(true) + setIsFailedConnection(false) + setIconGroup(TelephonyIcons.THREE_G) + setIsEmergencyOnly(false) + setNumberOfLevels(4) + isDataConnected.value = true + } + commonImpl = + MobileIconViewModel(SUB_1_ID, interactor, logger, constants, testScope.backgroundScope) + + homeIcon = HomeMobileIconViewModel(commonImpl, logger) + qsIcon = QsMobileIconViewModel(commonImpl, logger) + keyguardIcon = KeyguardMobileIconViewModel(commonImpl, logger) + } + + @Test + fun `location based view models receive same icon id when common impl updates`() = + testScope.runTest { + var latestHome: Int? = null + val homeJob = homeIcon.iconId.onEach { latestHome = it }.launchIn(this) + + var latestQs: Int? = null + val qsJob = qsIcon.iconId.onEach { latestQs = it }.launchIn(this) + + var latestKeyguard: Int? = null + val keyguardJob = keyguardIcon.iconId.onEach { latestKeyguard = it }.launchIn(this) + + var expected = defaultSignal(level = 1) + + assertThat(latestHome).isEqualTo(expected) + assertThat(latestQs).isEqualTo(expected) + assertThat(latestKeyguard).isEqualTo(expected) + + interactor.setLevel(2) + expected = defaultSignal(level = 2) + + assertThat(latestHome).isEqualTo(expected) + assertThat(latestQs).isEqualTo(expected) + assertThat(latestKeyguard).isEqualTo(expected) + + homeJob.cancel() + qsJob.cancel() + keyguardJob.cancel() + } + + companion object { + private const val SUB_1_ID = 1 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt index 415ce75345b2..50221bc97bad 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt @@ -22,32 +22,42 @@ import com.android.settingslib.mobile.TelephonyIcons.THREE_G import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import kotlinx.coroutines.yield import org.junit.Before import org.junit.Test import org.mockito.Mock import org.mockito.MockitoAnnotations +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest class MobileIconViewModelTest : SysuiTestCase() { private lateinit var underTest: MobileIconViewModel - private val interactor = FakeMobileIconInteractor() + private lateinit var interactor: FakeMobileIconInteractor @Mock private lateinit var logger: ConnectivityPipelineLogger @Mock private lateinit var constants: ConnectivityConstants + @Mock private lateinit var tableLogBuffer: TableLogBuffer + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) @Before fun setUp() { MockitoAnnotations.initMocks(this) + interactor = FakeMobileIconInteractor(tableLogBuffer) interactor.apply { setLevel(1) setIsDefaultDataEnabled(true) @@ -57,12 +67,13 @@ class MobileIconViewModelTest : SysuiTestCase() { setNumberOfLevels(4) isDataConnected.value = true } - underTest = MobileIconViewModel(SUB_1_ID, interactor, logger, constants) + underTest = + MobileIconViewModel(SUB_1_ID, interactor, logger, constants, testScope.backgroundScope) } @Test fun iconId_correctLevel_notCutout() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: Int? = null val job = underTest.iconId.onEach { latest = it }.launchIn(this) val expected = defaultSignal() @@ -74,7 +85,7 @@ class MobileIconViewModelTest : SysuiTestCase() { @Test fun iconId_cutout_whenDefaultDataDisabled() = - runBlocking(IMMEDIATE) { + testScope.runTest { interactor.setIsDefaultDataEnabled(false) var latest: Int? = null @@ -88,7 +99,7 @@ class MobileIconViewModelTest : SysuiTestCase() { @Test fun networkType_dataEnabled_groupIsRepresented() = - runBlocking(IMMEDIATE) { + testScope.runTest { val expected = Icon.Resource( THREE_G.dataType, @@ -106,7 +117,7 @@ class MobileIconViewModelTest : SysuiTestCase() { @Test fun networkType_nullWhenDisabled() = - runBlocking(IMMEDIATE) { + testScope.runTest { interactor.setIconGroup(THREE_G) interactor.setIsDataEnabled(false) var latest: Icon? = null @@ -119,7 +130,7 @@ class MobileIconViewModelTest : SysuiTestCase() { @Test fun networkType_nullWhenFailedConnection() = - runBlocking(IMMEDIATE) { + testScope.runTest { interactor.setIconGroup(THREE_G) interactor.setIsDataEnabled(true) interactor.setIsFailedConnection(true) @@ -133,7 +144,7 @@ class MobileIconViewModelTest : SysuiTestCase() { @Test fun networkType_nullWhenDataDisconnects() = - runBlocking(IMMEDIATE) { + testScope.runTest { val initial = Icon.Resource( THREE_G.dataType, @@ -157,7 +168,7 @@ class MobileIconViewModelTest : SysuiTestCase() { @Test fun networkType_null_changeToDisabled() = - runBlocking(IMMEDIATE) { + testScope.runTest { val expected = Icon.Resource( THREE_G.dataType, @@ -180,7 +191,7 @@ class MobileIconViewModelTest : SysuiTestCase() { @Test fun networkType_alwaysShow_shownEvenWhenDisabled() = - runBlocking(IMMEDIATE) { + testScope.runTest { interactor.setIconGroup(THREE_G) interactor.setIsDataEnabled(true) interactor.alwaysShowDataRatIcon.value = true @@ -200,7 +211,7 @@ class MobileIconViewModelTest : SysuiTestCase() { @Test fun networkType_alwaysShow_shownEvenWhenDisconnected() = - runBlocking(IMMEDIATE) { + testScope.runTest { interactor.setIconGroup(THREE_G) interactor.isDataConnected.value = false interactor.alwaysShowDataRatIcon.value = true @@ -220,7 +231,7 @@ class MobileIconViewModelTest : SysuiTestCase() { @Test fun networkType_alwaysShow_shownEvenWhenFailedConnection() = - runBlocking(IMMEDIATE) { + testScope.runTest { interactor.setIconGroup(THREE_G) interactor.setIsFailedConnection(true) interactor.alwaysShowDataRatIcon.value = true @@ -240,7 +251,7 @@ class MobileIconViewModelTest : SysuiTestCase() { @Test fun roaming() = - runBlocking(IMMEDIATE) { + testScope.runTest { interactor.isRoaming.value = true var latest: Boolean? = null val job = underTest.roaming.onEach { latest = it }.launchIn(this) @@ -256,10 +267,17 @@ class MobileIconViewModelTest : SysuiTestCase() { @Test fun `data activity - null when config is off`() = - runBlocking(IMMEDIATE) { + testScope.runTest { // Create a new view model here so the constants are properly read whenever(constants.shouldShowActivityConfig).thenReturn(false) - underTest = MobileIconViewModel(SUB_1_ID, interactor, logger, constants) + underTest = + MobileIconViewModel( + SUB_1_ID, + interactor, + logger, + constants, + testScope.backgroundScope, + ) var inVisible: Boolean? = null val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this) @@ -288,10 +306,17 @@ class MobileIconViewModelTest : SysuiTestCase() { @Test fun `data activity - config on - test indicators`() = - runBlocking(IMMEDIATE) { + testScope.runTest { // Create a new view model here so the constants are properly read whenever(constants.shouldShowActivityConfig).thenReturn(true) - underTest = MobileIconViewModel(SUB_1_ID, interactor, logger, constants) + underTest = + MobileIconViewModel( + SUB_1_ID, + interactor, + logger, + constants, + testScope.backgroundScope, + ) var inVisible: Boolean? = null val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this) @@ -340,16 +365,15 @@ class MobileIconViewModelTest : SysuiTestCase() { containerJob.cancel() } - /** Convenience constructor for these tests */ - private fun defaultSignal( - level: Int = 1, - connected: Boolean = true, - ): Int { - return SignalDrawable.getState(level, /* numLevels */ 4, !connected) - } - companion object { - private val IMMEDIATE = Dispatchers.Main.immediate private const val SUB_1_ID = 1 + + /** Convenience constructor for these tests */ + fun defaultSignal( + level: Int = 1, + connected: Boolean = true, + ): Int { + return SignalDrawable.getState(level, /* numLevels */ 4, !connected) + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt new file mode 100644 index 000000000000..d6cb76260f0b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.phone.StatusBarLocation +import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor +import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy +import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class MobileIconsViewModelTest : SysuiTestCase() { + private lateinit var underTest: MobileIconsViewModel + private val interactor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) + + @Mock private lateinit var logger: ConnectivityPipelineLogger + @Mock private lateinit var constants: ConnectivityConstants + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + val subscriptionIdsFlow = + interactor.filteredSubscriptions + .map { subs -> subs.map { it.subscriptionId } } + .stateIn(testScope.backgroundScope, SharingStarted.WhileSubscribed(), listOf()) + + underTest = + MobileIconsViewModel( + subscriptionIdsFlow, + interactor, + logger, + constants, + testScope.backgroundScope, + ) + + interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) + } + + @Test + fun `caching - mobile icon view model is reused for same sub id`() = + testScope.runTest { + val model1 = underTest.viewModelForSub(1, StatusBarLocation.HOME) + val model2 = underTest.viewModelForSub(1, StatusBarLocation.QS) + + assertThat(model1.commonImpl).isSameInstanceAs(model2.commonImpl) + } + + @Test + fun `caching - invalid view models are removed from cache when sub disappears`() = + testScope.runTest { + // Retrieve models to trigger caching + val model1 = underTest.viewModelForSub(1, StatusBarLocation.HOME) + val model2 = underTest.viewModelForSub(2, StatusBarLocation.QS) + + // Both impls are cached + assertThat(underTest.mobileIconSubIdCache) + .containsExactly(1, model1.commonImpl, 2, model2.commonImpl) + + // SUB_1 is removed from the list... + interactor.filteredSubscriptions.value = listOf(SUB_2) + + // ... and dropped from the cache + assertThat(underTest.mobileIconSubIdCache).containsExactly(2, model2.commonImpl) + } + + companion object { + private val SUB_1 = SubscriptionModel(subscriptionId = 1, isOpportunistic = false) + private val SUB_2 = SubscriptionModel(subscriptionId = 2, isOpportunistic = false) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt new file mode 100644 index 000000000000..b935442fd73a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.wifi.data.repository + +import android.net.ConnectivityManager +import android.net.wifi.WifiManager +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.demomode.DemoMode +import com.android.systemui.demomode.DemoModeController +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoWifiRepository +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.kotlinArgumentCaptor +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class WifiRepositorySwitcherTest : SysuiTestCase() { + private lateinit var underTest: WifiRepositorySwitcher + private lateinit var realImpl: WifiRepositoryImpl + private lateinit var demoImpl: DemoWifiRepository + + @Mock private lateinit var demoModeController: DemoModeController + @Mock private lateinit var logger: ConnectivityPipelineLogger + @Mock private lateinit var tableLogger: TableLogBuffer + @Mock private lateinit var connectivityManager: ConnectivityManager + @Mock private lateinit var wifiManager: WifiManager + @Mock private lateinit var demoModeWifiDataSource: DemoModeWifiDataSource + private val demoModelFlow = MutableStateFlow<FakeWifiEventModel?>(null) + + private val mainExecutor = FakeExecutor(FakeSystemClock()) + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + // Never start in demo mode + whenever(demoModeController.isInDemoMode).thenReturn(false) + + realImpl = + WifiRepositoryImpl( + fakeBroadcastDispatcher, + connectivityManager, + logger, + tableLogger, + mainExecutor, + testScope.backgroundScope, + wifiManager, + ) + + whenever(demoModeWifiDataSource.wifiEvents).thenReturn(demoModelFlow) + + demoImpl = + DemoWifiRepository( + demoModeWifiDataSource, + testScope.backgroundScope, + ) + + underTest = + WifiRepositorySwitcher( + realImpl, + demoImpl, + demoModeController, + testScope.backgroundScope, + ) + } + + @Test + fun `switcher active repo - updates when demo mode changes`() = + testScope.runTest { + assertThat(underTest.activeRepo.value).isSameInstanceAs(realImpl) + + var latest: WifiRepository? = null + val job = underTest.activeRepo.onEach { latest = it }.launchIn(this) + + startDemoMode() + + assertThat(latest).isSameInstanceAs(demoImpl) + + finishDemoMode() + + assertThat(latest).isSameInstanceAs(realImpl) + + job.cancel() + } + + private fun startDemoMode() { + whenever(demoModeController.isInDemoMode).thenReturn(true) + getDemoModeCallback().onDemoModeStarted() + } + + private fun finishDemoMode() { + whenever(demoModeController.isInDemoMode).thenReturn(false) + getDemoModeCallback().onDemoModeFinished() + } + + private fun getDemoModeCallback(): DemoMode { + val captor = kotlinArgumentCaptor<DemoMode>() + Mockito.verify(demoModeController).addCallback(captor.capture()) + return captor.value + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt index b47f177bbf24..41584347c0f2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt @@ -146,7 +146,7 @@ class WifiViewModelTest : SysuiTestCase() { @Test fun activity_showActivityConfigFalse_outputsFalse() = runBlocking(IMMEDIATE) { - whenever(wifiConstants.shouldShowActivityConfig).thenReturn(false) + whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(false) createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) @@ -183,7 +183,7 @@ class WifiViewModelTest : SysuiTestCase() { @Test fun activity_showActivityConfigFalse_noUpdatesReceived() = runBlocking(IMMEDIATE) { - whenever(wifiConstants.shouldShowActivityConfig).thenReturn(false) + whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(false) createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) @@ -225,7 +225,7 @@ class WifiViewModelTest : SysuiTestCase() { @Test fun activity_nullSsid_outputsFalse() = runBlocking(IMMEDIATE) { - whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, ssid = null)) @@ -268,7 +268,7 @@ class WifiViewModelTest : SysuiTestCase() { @Test fun activity_allLocationViewModelsReceiveSameData() = runBlocking(IMMEDIATE) { - whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) @@ -308,7 +308,7 @@ class WifiViewModelTest : SysuiTestCase() { @Test fun activityIn_hasActivityInTrue_outputsTrue() = runBlocking(IMMEDIATE) { - whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) @@ -330,7 +330,7 @@ class WifiViewModelTest : SysuiTestCase() { @Test fun activityIn_hasActivityInFalse_outputsFalse() = runBlocking(IMMEDIATE) { - whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) @@ -352,7 +352,7 @@ class WifiViewModelTest : SysuiTestCase() { @Test fun activityOut_hasActivityOutTrue_outputsTrue() = runBlocking(IMMEDIATE) { - whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) @@ -374,7 +374,7 @@ class WifiViewModelTest : SysuiTestCase() { @Test fun activityOut_hasActivityOutFalse_outputsFalse() = runBlocking(IMMEDIATE) { - whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) @@ -396,7 +396,7 @@ class WifiViewModelTest : SysuiTestCase() { @Test fun activityContainer_hasActivityInTrue_outputsTrue() = runBlocking(IMMEDIATE) { - whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) @@ -418,7 +418,7 @@ class WifiViewModelTest : SysuiTestCase() { @Test fun activityContainer_hasActivityOutTrue_outputsTrue() = runBlocking(IMMEDIATE) { - whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) @@ -440,7 +440,7 @@ class WifiViewModelTest : SysuiTestCase() { @Test fun activityContainer_inAndOutTrue_outputsTrue() = runBlocking(IMMEDIATE) { - whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) @@ -462,7 +462,7 @@ class WifiViewModelTest : SysuiTestCase() { @Test fun activityContainer_inAndOutFalse_outputsFalse() = runBlocking(IMMEDIATE) { - whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 55019490bdcd..39d2ecaef51a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -46,12 +46,18 @@ class FakeKeyguardRepository : KeyguardRepository { private val _isKeyguardShowing = MutableStateFlow(false) override val isKeyguardShowing: Flow<Boolean> = _isKeyguardShowing + private val _isKeyguardOccluded = MutableStateFlow(false) + override val isKeyguardOccluded: Flow<Boolean> = _isKeyguardOccluded + private val _isDozing = MutableStateFlow(false) override val isDozing: Flow<Boolean> = _isDozing private val _isDreaming = MutableStateFlow(false) override val isDreaming: Flow<Boolean> = _isDreaming + private val _isDreamingWithOverlay = MutableStateFlow(false) + override val isDreamingWithOverlay: Flow<Boolean> = _isDreamingWithOverlay + private val _dozeAmount = MutableStateFlow(0f) override val linearDozeAmount: Flow<Float> = _dozeAmount @@ -112,10 +118,18 @@ class FakeKeyguardRepository : KeyguardRepository { _isKeyguardShowing.value = isShowing } + fun setKeyguardOccluded(isOccluded: Boolean) { + _isKeyguardOccluded.value = isOccluded + } + fun setDozing(isDozing: Boolean) { _isDozing.value = isDozing } + fun setDreamingWithOverlay(isDreaming: Boolean) { + _isDreamingWithOverlay.value = isDreaming + } + fun setDozeAmount(dozeAmount: Float) { _dozeAmount.value = dozeAmount } @@ -144,6 +158,10 @@ class FakeKeyguardRepository : KeyguardRepository { _fingerprintSensorLocation.tryEmit(location) } + fun setDozeTransitionModel(model: DozeTransitionModel) { + _dozeTransitionModel.value = model + } + override fun isUdfpsSupported(): Boolean { return _isUdfpsSupported.value } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java index b16ca8b92848..b4a294d09b7e 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java @@ -19,6 +19,7 @@ package com.android.server.notification; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.fail; +import android.os.Parcel; import android.service.notification.ZenPolicy; import android.service.notification.nano.DNDPolicyProto; import android.test.suitebuilder.annotation.SmallTest; @@ -32,9 +33,13 @@ import com.google.protobuf.nano.InvalidProtocolBufferNanoException; import org.junit.Test; import org.junit.runner.RunWith; +import java.lang.reflect.Field; +import java.util.ArrayList; + @SmallTest @RunWith(AndroidJUnit4.class) public class ZenPolicyTest extends UiServiceTestCase { + private static final String CLASS = "android.service.notification.ZenPolicy"; @Test public void testZenPolicyApplyAllowedToDisallowed() { @@ -524,6 +529,66 @@ public class ZenPolicyTest extends UiServiceTestCase { assertProtoMatches(policy, policy.toProto()); } + @Test + public void testTooLongLists_fromParcel() { + ArrayList<Integer> longList = new ArrayList<Integer>(50); + for (int i = 0; i < 50; i++) { + longList.add(ZenPolicy.STATE_UNSET); + } + + ZenPolicy.Builder builder = new ZenPolicy.Builder(); + ZenPolicy policy = builder.build(); + + try { + Field priorityCategories = Class.forName(CLASS).getDeclaredField( + "mPriorityCategories"); + priorityCategories.setAccessible(true); + priorityCategories.set(policy, longList); + + Field visualEffects = Class.forName(CLASS).getDeclaredField("mVisualEffects"); + visualEffects.setAccessible(true); + visualEffects.set(policy, longList); + } catch (NoSuchFieldException e) { + fail(e.toString()); + } catch (ClassNotFoundException e) { + fail(e.toString()); + } catch (IllegalAccessException e) { + fail(e.toString()); + } + + Parcel parcel = Parcel.obtain(); + policy.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + ZenPolicy fromParcel = ZenPolicy.CREATOR.createFromParcel(parcel); + + // Confirm that all the fields are accessible and UNSET + assertAllPriorityCategoriesUnsetExcept(fromParcel, -1); + assertAllVisualEffectsUnsetExcept(fromParcel, -1); + + // Because we don't access the lists directly, we also need to use reflection to make sure + // the lists are the right length. + try { + Field priorityCategories = Class.forName(CLASS).getDeclaredField( + "mPriorityCategories"); + priorityCategories.setAccessible(true); + ArrayList<Integer> pcList = (ArrayList<Integer>) priorityCategories.get(fromParcel); + assertEquals(ZenPolicy.NUM_PRIORITY_CATEGORIES, pcList.size()); + + + Field visualEffects = Class.forName(CLASS).getDeclaredField("mVisualEffects"); + visualEffects.setAccessible(true); + ArrayList<Integer> veList = (ArrayList<Integer>) visualEffects.get(fromParcel); + assertEquals(ZenPolicy.NUM_VISUAL_EFFECTS, veList.size()); + } catch (NoSuchFieldException e) { + fail(e.toString()); + } catch (ClassNotFoundException e) { + fail(e.toString()); + } catch (IllegalAccessException e) { + fail(e.toString()); + } + } + private void assertAllPriorityCategoriesUnsetExcept(ZenPolicy policy, int except) { if (except != ZenPolicy.PRIORITY_CATEGORY_REMINDERS) { assertEquals(ZenPolicy.STATE_UNSET, policy.getPriorityCategoryReminders()); |