diff options
71 files changed, 4164 insertions, 822 deletions
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index cb64173b7809..9344d96d2893 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -9134,8 +9134,9 @@ public class AppOpsManager { */ public int startProxyOpNoThrow(int op, @NonNull AttributionSource attributionSource, @Nullable String message, boolean skipProxyOperation) { - return startProxyOpNoThrow(op, attributionSource, message, skipProxyOperation, - ATTRIBUTION_FLAGS_NONE, ATTRIBUTION_FLAGS_NONE, ATTRIBUTION_CHAIN_ID_NONE); + return startProxyOpNoThrow(attributionSource.getToken(), op, attributionSource, message, + skipProxyOperation, ATTRIBUTION_FLAGS_NONE, ATTRIBUTION_FLAGS_NONE, + ATTRIBUTION_CHAIN_ID_NONE); } /** @@ -9147,7 +9148,8 @@ public class AppOpsManager { * * @hide */ - public int startProxyOpNoThrow(int op, @NonNull AttributionSource attributionSource, + public int startProxyOpNoThrow(@NonNull IBinder clientId, int op, + @NonNull AttributionSource attributionSource, @Nullable String message, boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags, @AttributionFlags int proxiedAttributionFlags, int attributionChainId) { @@ -9165,7 +9167,7 @@ public class AppOpsManager { } } - SyncNotedAppOp syncOp = mService.startProxyOperation(op, + SyncNotedAppOp syncOp = mService.startProxyOperation(clientId, op, attributionSource, false, collectionMode == COLLECT_ASYNC, message, shouldCollectMessage, skipProxyOperation, proxyAttributionFlags, proxiedAttributionFlags, attributionChainId); @@ -9263,9 +9265,10 @@ public class AppOpsManager { */ public void finishProxyOp(@NonNull String op, int proxiedUid, @NonNull String proxiedPackageName, @Nullable String proxiedAttributionTag) { - finishProxyOp(op, new AttributionSource(mContext.getAttributionSource(), + IBinder token = mContext.getAttributionSource().getToken(); + finishProxyOp(token, op, new AttributionSource(mContext.getAttributionSource(), new AttributionSource(proxiedUid, proxiedPackageName, proxiedAttributionTag, - mContext.getAttributionSource().getToken())), /*skipProxyOperation*/ false); + token)), /*skipProxyOperation*/ false); } /** @@ -9280,10 +9283,11 @@ public class AppOpsManager { * * @hide */ - public void finishProxyOp(@NonNull String op, @NonNull AttributionSource attributionSource, - boolean skipProxyOperation) { + public void finishProxyOp(@NonNull IBinder clientId, @NonNull String op, + @NonNull AttributionSource attributionSource, boolean skipProxyOperation) { try { - mService.finishProxyOperation(strOpToOp(op), attributionSource, skipProxyOperation); + mService.finishProxyOperation(clientId, strOpToOp(op), attributionSource, + skipProxyOperation); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/app/AppOpsManagerInternal.java b/core/java/android/app/AppOpsManagerInternal.java index 4d6e4aedba66..43023fe9c2ab 100644 --- a/core/java/android/app/AppOpsManagerInternal.java +++ b/core/java/android/app/AppOpsManagerInternal.java @@ -26,13 +26,11 @@ import android.util.SparseArray; import android.util.SparseIntArray; import com.android.internal.app.IAppOpsCallback; -import com.android.internal.util.function.DecFunction; import com.android.internal.util.function.HeptFunction; import com.android.internal.util.function.HexFunction; import com.android.internal.util.function.QuadFunction; import com.android.internal.util.function.QuintConsumer; import com.android.internal.util.function.QuintFunction; -import com.android.internal.util.function.TriFunction; import com.android.internal.util.function.UndecFunction; /** @@ -135,6 +133,7 @@ public abstract class AppOpsManagerInternal { /** * Allows overriding start proxy operation behavior. * + * @param clientId The client calling start, represented by an IBinder * @param code The op code to start. * @param attributionSource The permission identity of the caller. * @param startIfModeDefault Whether to start the op of the mode is default. @@ -148,11 +147,12 @@ public abstract class AppOpsManagerInternal { * @param superImpl The super implementation. * @return The app op note result. */ - SyncNotedAppOp startProxyOperation(int code, @NonNull AttributionSource attributionSource, - boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, String message, - boolean shouldCollectMessage, boolean skipProxyOperation, @AttributionFlags - int proxyAttributionFlags, @AttributionFlags int proxiedAttributionFlags, - int attributionChainId, @NonNull DecFunction<Integer, AttributionSource, Boolean, + SyncNotedAppOp startProxyOperation(@NonNull IBinder clientId, int code, + @NonNull AttributionSource attributionSource, boolean startIfModeDefault, + boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage, + boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags, + @AttributionFlags int proxiedAttributionFlags, int attributionChainId, + @NonNull UndecFunction<IBinder, Integer, AttributionSource, Boolean, Boolean, String, Boolean, Boolean, Integer, Integer, Integer, SyncNotedAppOp> superImpl); @@ -176,10 +176,15 @@ public abstract class AppOpsManagerInternal { * * @param code The op code to finish. * @param attributionSource The permission identity of the caller. + * @param skipProxyOperation Whether to skip the proxy in the proxy/proxied operation + * @param clientId The client calling finishProxyOperation + * @param superImpl The "standard" implementation to potentially call */ - void finishProxyOperation(int code, @NonNull AttributionSource attributionSource, + void finishProxyOperation(@NonNull IBinder clientId, int code, + @NonNull AttributionSource attributionSource, boolean skipProxyOperation, - @NonNull TriFunction<Integer, AttributionSource, Boolean, Void> superImpl); + @NonNull QuadFunction<IBinder, Integer, AttributionSource, Boolean, + Void> superImpl); } /** diff --git a/core/java/android/service/controls/ControlsProviderService.java b/core/java/android/service/controls/ControlsProviderService.java index d2a4ae282061..9396a888ec13 100644 --- a/core/java/android/service/controls/ControlsProviderService.java +++ b/core/java/android/service/controls/ControlsProviderService.java @@ -69,6 +69,18 @@ public abstract class ControlsProviderService extends Service { "android.service.controls.META_DATA_PANEL_ACTIVITY"; /** + * Boolean extra containing the value of + * {@link android.provider.Settings.Secure#LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS}. + * + * This is passed with the intent when the panel specified by {@link #META_DATA_PANEL_ACTIVITY} + * is launched. + * + * @hide + */ + public static final String EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS = + "android.service.controls.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS"; + + /** * @hide */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) diff --git a/core/java/com/android/internal/app/IAppOpsService.aidl b/core/java/com/android/internal/app/IAppOpsService.aidl index 30da4b470ab6..88447daf7338 100644 --- a/core/java/com/android/internal/app/IAppOpsService.aidl +++ b/core/java/com/android/internal/app/IAppOpsService.aidl @@ -58,11 +58,12 @@ interface IAppOpsService { SyncNotedAppOp noteProxyOperation(int code, in AttributionSource attributionSource, boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage, boolean skipProxyOperation); - SyncNotedAppOp startProxyOperation(int code, in AttributionSource attributionSource, - boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, String message, - boolean shouldCollectMessage, boolean skipProxyOperation, int proxyAttributionFlags, - int proxiedAttributionFlags, int attributionChainId); - void finishProxyOperation(int code, in AttributionSource attributionSource, + SyncNotedAppOp startProxyOperation(IBinder clientId, int code, + in AttributionSource attributionSource, boolean startIfModeDefault, + boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage, + boolean skipProxyOperation, int proxyAttributionFlags, int proxiedAttributionFlags, + int attributionChainId); + void finishProxyOperation(IBinder clientId, int code, in AttributionSource attributionSource, boolean skipProxyOperation); // Remaining methods are only used in Java. diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index 2cf41bbfffc1..017bf3f3dd6c 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -331,4 +331,11 @@ oneway interface IStatusBar /** Called when requested to go to fullscreen from the active split app. */ void goToFullscreenFromSplit(); + + /** + * Enters stage split from a current running app. + * + * @param leftOrTop indicates where the stage split is. + */ + void enterStageSplitFromRunningApp(boolean leftOrTop); } diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 5763345aba4d..9410e0682106 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -58,6 +58,9 @@ <!-- Displayed when the user dialed an MMI code whose function could not be performed. This will be displayed in a toast. --> <string name="mmiError">Connection problem or invalid MMI code.</string> + <!-- Displayed when the user dialed an MMI code whose function could not be performed because + the feature is not supported on the current mobile network. --> + <string name="mmiErrorNotSupported">Feature not supported.</string> <!-- Displayed when the user dialed an MMI code whose function could not be performed because FDN is enabled. This will be displayed in a toast. --> <string name="mmiFdnError">Operation is restricted to fixed dialing numbers only.</string> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 694040aa9b2d..9722e79b700a 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -837,6 +837,7 @@ <java-symbol type="string" name="mismatchPin" /> <java-symbol type="string" name="mmiComplete" /> <java-symbol type="string" name="mmiError" /> + <java-symbol type="string" name="mmiErrorNotSupported" /> <java-symbol type="string" name="mmiFdnError" /> <java-symbol type="string" name="mmiErrorWhileRoaming" /> <java-symbol type="string" name="month_day_year" /> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java index 6e116b958ac9..c836b95ffab8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java @@ -51,8 +51,6 @@ import com.android.wm.shell.R; import com.android.wm.shell.common.ScreenshotUtils; import com.android.wm.shell.common.SurfaceUtils; -import java.util.function.Consumer; - /** * Handles split decor like showing resizing hint for a specific split. */ @@ -72,17 +70,18 @@ public class SplitDecorManager extends WindowlessWindowManager { private SurfaceControl mIconLeash; private SurfaceControl mBackgroundLeash; private SurfaceControl mGapBackgroundLeash; + private SurfaceControl mScreenshot; private boolean mShown; private boolean mIsResizing; private final Rect mBounds = new Rect(); - private final Rect mResizingBounds = new Rect(); private final Rect mTempRect = new Rect(); private ValueAnimator mFadeAnimator; private int mIconSize; private int mOffsetX; private int mOffsetY; + private int mRunningAnimationCount = 0; public SplitDecorManager(Configuration configuration, IconProvider iconProvider, SurfaceSession surfaceSession) { @@ -173,7 +172,6 @@ public class SplitDecorManager extends WindowlessWindowManager { mIsResizing = true; mBounds.set(newBounds); } - mResizingBounds.set(newBounds); mOffsetX = offsetX; mOffsetY = offsetY; @@ -227,33 +225,41 @@ public class SplitDecorManager extends WindowlessWindowManager { t.setVisibility(mBackgroundLeash, show); t.setVisibility(mIconLeash, show); } else { - startFadeAnimation(show, null /* finishedConsumer */); + startFadeAnimation(show, false, null); } mShown = show; } } /** Stops showing resizing hint. */ - public void onResized(SurfaceControl.Transaction t) { - if (!mShown && mIsResizing) { - mTempRect.set(mResizingBounds); - mTempRect.offsetTo(-mOffsetX, -mOffsetY); - final SurfaceControl screenshot = ScreenshotUtils.takeScreenshot(t, - mHostLeash, mTempRect, Integer.MAX_VALUE - 1); + public void onResized(SurfaceControl.Transaction t, Runnable animFinishedCallback) { + if (mScreenshot != null) { + t.setPosition(mScreenshot, mOffsetX, mOffsetY); final SurfaceControl.Transaction animT = new SurfaceControl.Transaction(); final ValueAnimator va = ValueAnimator.ofFloat(1, 0); va.addUpdateListener(valueAnimator -> { final float progress = (float) valueAnimator.getAnimatedValue(); - animT.setAlpha(screenshot, progress); + animT.setAlpha(mScreenshot, progress); animT.apply(); }); va.addListener(new AnimatorListenerAdapter() { @Override + public void onAnimationStart(Animator animation) { + mRunningAnimationCount++; + } + + @Override public void onAnimationEnd(@androidx.annotation.NonNull Animator animation) { - animT.remove(screenshot); + mRunningAnimationCount--; + animT.remove(mScreenshot); animT.apply(); animT.close(); + mScreenshot = null; + + if (mRunningAnimationCount == 0 && animFinishedCallback != null) { + animFinishedCallback.run(); + } } }); va.start(); @@ -285,10 +291,34 @@ public class SplitDecorManager extends WindowlessWindowManager { mFadeAnimator.cancel(); } if (mShown) { - fadeOutDecor(null /* finishedCallback */); + fadeOutDecor(animFinishedCallback); } else { // Decor surface is hidden so release it directly. releaseDecor(t); + if (mRunningAnimationCount == 0 && animFinishedCallback != null) { + animFinishedCallback.run(); + } + } + } + + /** Screenshot host leash and attach on it if meet some conditions */ + public void screenshotIfNeeded(SurfaceControl.Transaction t) { + if (!mShown && mIsResizing) { + mTempRect.set(mBounds); + mTempRect.offsetTo(0, 0); + mScreenshot = ScreenshotUtils.takeScreenshot(t, mHostLeash, mTempRect, + Integer.MAX_VALUE - 1); + } + } + + /** Set screenshot and attach on host leash it if meet some conditions */ + public void setScreenshotIfNeeded(SurfaceControl screenshot, SurfaceControl.Transaction t) { + if (screenshot == null || !screenshot.isValid()) return; + + if (!mShown && mIsResizing) { + mScreenshot = screenshot; + t.reparent(screenshot, mHostLeash); + t.setLayer(screenshot, Integer.MAX_VALUE - 1); } } @@ -296,18 +326,15 @@ public class SplitDecorManager extends WindowlessWindowManager { * directly. */ public void fadeOutDecor(Runnable finishedCallback) { if (mShown) { - startFadeAnimation(false /* show */, transaction -> { - releaseDecor(transaction); - if (finishedCallback != null) finishedCallback.run(); - }); + startFadeAnimation(false /* show */, true, finishedCallback); mShown = false; } else { if (finishedCallback != null) finishedCallback.run(); } } - private void startFadeAnimation(boolean show, - Consumer<SurfaceControl.Transaction> finishedConsumer) { + private void startFadeAnimation(boolean show, boolean releaseSurface, + Runnable finishedCallback) { final SurfaceControl.Transaction animT = new SurfaceControl.Transaction(); mFadeAnimator = ValueAnimator.ofFloat(0f, 1f); mFadeAnimator.setDuration(FADE_DURATION); @@ -324,6 +351,7 @@ public class SplitDecorManager extends WindowlessWindowManager { mFadeAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(@NonNull Animator animation) { + mRunningAnimationCount++; if (show) { animT.show(mBackgroundLeash).show(mIconLeash); } @@ -335,6 +363,7 @@ public class SplitDecorManager extends WindowlessWindowManager { @Override public void onAnimationEnd(@NonNull Animator animation) { + mRunningAnimationCount--; if (!show) { if (mBackgroundLeash != null) { animT.hide(mBackgroundLeash); @@ -343,11 +372,15 @@ public class SplitDecorManager extends WindowlessWindowManager { animT.hide(mIconLeash); } } - if (finishedConsumer != null) { - finishedConsumer.accept(animT); + if (releaseSurface) { + releaseDecor(animT); } animT.apply(); animT.close(); + + if (mRunningAnimationCount == 0 && finishedCallback != null) { + finishedCallback.run(); + } } }); mFadeAnimator.start(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index 400039b32618..9329d021d007 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -529,10 +529,24 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) { Intent fillInIntent = null; - if (launchSameComponentAdjacently(pendingIntent, splitPosition, taskId) - && supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) { - fillInIntent = new Intent(); - fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + if (launchSameComponentAdjacently(pendingIntent, splitPosition, taskId)) { + if (supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) { + fillInIntent = new Intent(); + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); + } else { + try { + adapter.getRunner().onAnimationCancelled(false /* isKeyguardOccluded */); + ActivityTaskManager.getService().startActivityFromRecents(taskId, options2); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "Cancel entering split as not supporting multi-instances"); + Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text, + Toast.LENGTH_SHORT).show(); + return; + } } mStageCoordinator.startIntentAndTaskWithLegacyTransition(pendingIntent, fillInIntent, options1, taskId, options2, splitPosition, splitRatio, adapter, instanceId); @@ -542,10 +556,17 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { Intent fillInIntent = null; - if (launchSameComponentAdjacently(pendingIntent, splitPosition, taskId) - && supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) { - fillInIntent = new Intent(); - fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + if (launchSameComponentAdjacently(pendingIntent, splitPosition, taskId)) { + if (supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) { + fillInIntent = new Intent(); + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); + } else { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "Cancel entering split as not supporting multi-instances"); + Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text, + Toast.LENGTH_SHORT).show(); + } } mStageCoordinator.startIntentAndTask(pendingIntent, fillInIntent, options1, taskId, options2, splitPosition, splitRatio, remoteTransition, instanceId); @@ -557,12 +578,26 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) { Intent fillInIntent1 = null; Intent fillInIntent2 = null; - if (launchSameComponentAdjacently(pendingIntent1, pendingIntent2) - && supportMultiInstancesSplit(pendingIntent1.getIntent().getComponent())) { - fillInIntent1 = new Intent(); - fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); - fillInIntent2 = new Intent(); - fillInIntent2.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + if (launchSameComponentAdjacently(pendingIntent1, pendingIntent2)) { + if (supportMultiInstancesSplit(pendingIntent1.getIntent().getComponent())) { + fillInIntent1 = new Intent(); + fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + fillInIntent2 = new Intent(); + fillInIntent2.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); + } else { + try { + adapter.getRunner().onAnimationCancelled(false /* isKeyguardOccluded */); + pendingIntent1.send(); + } catch (RemoteException | PendingIntent.CanceledException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "Cancel entering split as not supporting multi-instances"); + Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text, + Toast.LENGTH_SHORT).show(); + return; + } } mStageCoordinator.startIntentsWithLegacyTransition(pendingIntent1, fillInIntent1, options1, pendingIntent2, fillInIntent2, options2, splitPosition, splitRatio, adapter, @@ -601,6 +636,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mStageCoordinator.switchSplitPosition("startIntent"); return; } else { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "Cancel entering split as not supporting multi-instances"); Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text, Toast.LENGTH_SHORT).show(); return; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java index 21a13103616c..1cf3a896b68e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java @@ -47,6 +47,7 @@ import android.window.WindowContainerTransactionCallback; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.common.split.SplitDecorManager; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.transition.OneShotRemoteHandler; import com.android.wm.shell.transition.Transitions; @@ -64,6 +65,7 @@ class SplitScreenTransitions { DismissTransition mPendingDismiss = null; TransitSession mPendingEnter = null; TransitSession mPendingRecent = null; + TransitSession mPendingResize = null; private IBinder mAnimatingTransition = null; OneShotRemoteHandler mPendingRemoteHandler = null; @@ -177,6 +179,43 @@ class SplitScreenTransitions { onFinish(null /* wct */, null /* wctCB */); } + void applyResizeTransition(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback, + @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot, + @NonNull SplitDecorManager mainDecor, @NonNull SplitDecorManager sideDecor) { + mFinishCallback = finishCallback; + mAnimatingTransition = transition; + mFinishTransaction = finishTransaction; + + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (mainRoot.equals(change.getContainer()) || sideRoot.equals(change.getContainer())) { + final SurfaceControl leash = change.getLeash(); + startTransaction.setPosition(leash, change.getEndAbsBounds().left, + change.getEndAbsBounds().top); + startTransaction.setWindowCrop(leash, change.getEndAbsBounds().width(), + change.getEndAbsBounds().height()); + + SplitDecorManager decor = mainRoot.equals(change.getContainer()) + ? mainDecor : sideDecor; + ValueAnimator va = new ValueAnimator(); + mAnimations.add(va); + decor.setScreenshotIfNeeded(change.getSnapshot(), startTransaction); + decor.onResized(startTransaction, () -> { + mTransitions.getMainExecutor().execute(() -> { + mAnimations.remove(va); + onFinish(null /* wct */, null /* wctCB */); + }); + }); + } + } + + startTransaction.apply(); + onFinish(null /* wct */, null /* wctCB */); + } + boolean isPendingTransition(IBinder transition) { return getPendingTransition(transition) != null; } @@ -193,6 +232,10 @@ class SplitScreenTransitions { return mPendingDismiss != null && mPendingDismiss.mTransition == transition; } + boolean isPendingResize(IBinder transition) { + return mPendingResize != null && mPendingResize.mTransition == transition; + } + @Nullable private TransitSession getPendingTransition(IBinder transition) { if (isPendingEnter(transition)) { @@ -201,11 +244,14 @@ class SplitScreenTransitions { return mPendingRecent; } else if (isPendingDismiss(transition)) { return mPendingDismiss; + } else if (isPendingResize(transition)) { + return mPendingResize; } return null; } + /** Starts a transition to enter split with a remote transition animator. */ IBinder startEnterTransition( @WindowManager.TransitionType int transitType, @@ -258,6 +304,21 @@ class SplitScreenTransitions { exitReasonToString(reason), stageTypeToString(dismissTop)); } + IBinder startResizeTransition(WindowContainerTransaction wct, + Transitions.TransitionHandler handler, + @Nullable TransitionFinishedCallback finishCallback) { + IBinder transition = mTransitions.startTransition(TRANSIT_CHANGE, wct, handler); + setResizeTransition(transition, finishCallback); + return transition; + } + + void setResizeTransition(@NonNull IBinder transition, + @Nullable TransitionFinishedCallback finishCallback) { + mPendingResize = new TransitSession(transition, null /* consumedCb */, finishCallback); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition " + + " deduced Resize split screen"); + } + void setRecentTransition(@NonNull IBinder transition, @Nullable RemoteTransition remoteTransition, @Nullable TransitionFinishedCallback finishCallback) { @@ -324,6 +385,9 @@ class SplitScreenTransitions { mPendingRecent.onConsumed(aborted); mPendingRecent = null; mPendingRemoteHandler = null; + } else if (isPendingResize(transition)) { + mPendingResize.onConsumed(aborted); + mPendingResize = null; } } @@ -340,6 +404,9 @@ class SplitScreenTransitions { } else if (isPendingDismiss(mAnimatingTransition)) { mPendingDismiss.onFinished(wct, mFinishTransaction); mPendingDismiss = null; + } else if (isPendingResize(mAnimatingTransition)) { + mPendingResize.onFinished(wct, mFinishTransaction); + mPendingResize = null; } mPendingRemoteHandler = null; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java index 2dc4a0441b06..1016e1bcd66f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java @@ -23,6 +23,7 @@ import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED_ import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__FULLSCREEN_SHORTCUT; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__ROOT_TASK_VANISHED; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED; @@ -38,6 +39,7 @@ import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DEVICE_FOLDED; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_SHORTCUT; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_ROOT_TASK_VANISHED; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_SCREEN_LOCKED; @@ -180,6 +182,8 @@ public class SplitscreenEventLogger { return SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED; case EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP: return SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP; + case EXIT_REASON_FULLSCREEN_SHORTCUT: + return SPLITSCREEN_UICHANGED__EXIT_REASON__FULLSCREEN_SHORTCUT; case EXIT_REASON_UNKNOWN: // Fall through default: diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index aa0512b64a16..da8dc8733ef5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -456,8 +456,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, void startIntentLegacy(PendingIntent intent, Intent fillInIntent, @SplitPosition int position, @Nullable Bundle options) { final boolean isEnteringSplit = !isSplitActive(); - final WindowContainerTransaction evictWct = new WindowContainerTransaction(); - prepareEvictChildTasks(position, evictWct); LegacyTransitions.ILegacyTransition transition = new LegacyTransitions.ILegacyTransition() { @Override @@ -465,22 +463,21 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, IRemoteAnimationFinishedCallback finishedCallback, SurfaceControl.Transaction t) { - if (isEnteringSplit) { - boolean openingToSide = false; - if (apps != null) { - for (int i = 0; i < apps.length; ++i) { - if (apps[i].mode == MODE_OPENING - && mSideStage.containsTask(apps[i].taskId)) { - openingToSide = true; - break; - } + boolean openingToSide = false; + if (apps != null) { + for (int i = 0; i < apps.length; ++i) { + if (apps[i].mode == MODE_OPENING + && mSideStage.containsTask(apps[i].taskId)) { + openingToSide = true; + break; } } - if (!openingToSide) { - mMainExecutor.execute(() -> exitSplitScreen( - mSideStage.getChildCount() == 0 ? mMainStage : mSideStage, - EXIT_REASON_UNKNOWN)); - } + } + + if (isEnteringSplit && !openingToSide) { + mMainExecutor.execute(() -> exitSplitScreen( + mSideStage.getChildCount() == 0 ? mMainStage : mSideStage, + EXIT_REASON_UNKNOWN)); } if (apps != null) { @@ -500,7 +497,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } - mSyncQueue.queue(evictWct); + + if (!isEnteringSplit && openingToSide) { + final WindowContainerTransaction evictWct = new WindowContainerTransaction(); + prepareEvictNonOpeningChildTasks(position, apps, evictWct); + mSyncQueue.queue(evictWct); + } } }; @@ -1667,15 +1669,29 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, public void onLayoutSizeChanged(SplitLayout layout) { // Reset this flag every time onLayoutSizeChanged. mShowDecorImmediately = false; + + if (!ENABLE_SHELL_TRANSITIONS) { + // Only need screenshot for legacy case because shell transition should screenshot + // itself during transition. + final SurfaceControl.Transaction startT = mTransactionPool.acquire(); + mMainStage.screenshotIfNeeded(startT); + mSideStage.screenshotIfNeeded(startT); + mTransactionPool.release(startT); + } + final WindowContainerTransaction wct = new WindowContainerTransaction(); updateWindowBounds(layout, wct); sendOnBoundsChanged(); - mSyncQueue.queue(wct); - mSyncQueue.runInSync(t -> { - updateSurfaceBounds(layout, t, false /* applyResizingOffset */); - mMainStage.onResized(t); - mSideStage.onResized(t); - }); + if (ENABLE_SHELL_TRANSITIONS) { + mSplitTransitions.startResizeTransition(wct, this, null /* callback */); + } else { + mSyncQueue.queue(wct); + mSyncQueue.runInSync(t -> { + updateSurfaceBounds(layout, t, false /* applyResizingOffset */); + mMainStage.onResized(t); + mSideStage.onResized(t); + }); + } mLogger.logResize(mSplitLayout.getDividerPositionAsFraction()); } @@ -2029,6 +2045,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } else if (mSplitTransitions.isPendingDismiss(transition)) { shouldAnimate = startPendingDismissAnimation( mSplitTransitions.mPendingDismiss, info, startTransaction, finishTransaction); + } else if (mSplitTransitions.isPendingResize(transition)) { + mSplitTransitions.applyResizeTransition(transition, info, startTransaction, + finishTransaction, finishCallback, mMainStage.mRootTaskInfo.token, + mSideStage.mRootTaskInfo.token, mMainStage.getSplitDecorManager(), + mSideStage.getSplitDecorManager()); + return true; } if (!shouldAnimate) return false; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java index 358f712f76b5..8a52c8750ba6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java @@ -292,7 +292,13 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { void onResized(SurfaceControl.Transaction t) { if (mSplitDecorManager != null) { - mSplitDecorManager.onResized(t); + mSplitDecorManager.onResized(t, null); + } + } + + void screenshotIfNeeded(SurfaceControl.Transaction t) { + if (mSplitDecorManager != null) { + mSplitDecorManager.screenshotIfNeeded(t); } } @@ -304,6 +310,10 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { } } + SplitDecorManager getSplitDecorManager() { + return mSplitDecorManager; + } + void addTask(ActivityManager.RunningTaskInfo task, WindowContainerTransaction wct) { // Clear overridden bounds and windowing mode to make sure the child task can inherit // windowing mode and bounds from split root. diff --git a/packages/SystemUI/animation/Android.bp b/packages/SystemUI/animation/Android.bp index 17ad55f403fb..8acc2f8adcf9 100644 --- a/packages/SystemUI/animation/Android.bp +++ b/packages/SystemUI/animation/Android.bp @@ -44,23 +44,3 @@ android_library { manifest: "AndroidManifest.xml", kotlincflags: ["-Xjvm-default=all"], } - -android_test { - name: "SystemUIAnimationLibTests", - - static_libs: [ - "SystemUIAnimationLib", - "androidx.test.ext.junit", - "androidx.test.rules", - "testables", - ], - libs: [ - "android.test.base", - ], - srcs: [ - "**/*.java", - "**/*.kt", - ], - kotlincflags: ["-Xjvm-default=all"], - test_suites: ["general-tests"], -} diff --git a/packages/SystemUI/animation/TEST_MAPPING b/packages/SystemUI/animation/TEST_MAPPING deleted file mode 100644 index 3dc8510bbf24..000000000000 --- a/packages/SystemUI/animation/TEST_MAPPING +++ /dev/null @@ -1,7 +0,0 @@ -{ - "presubmit": [ - { - "name": "SystemUIAnimationLibTests" - } - ] -} diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index 02a6d7be7143..e6f559b3da5f 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -210,8 +210,10 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { (FaceScanningOverlay) getOverlayView(mFaceScanningViewId); if (faceScanningOverlay != null) { faceScanningOverlay.setHideOverlayRunnable(() -> { + Trace.beginSection("ScreenDecorations#hideOverlayRunnable"); updateOverlayWindowVisibilityIfViewExists( faceScanningOverlay.findViewById(mFaceScanningViewId)); + Trace.endSection(); }); faceScanningOverlay.enableShowProtection(false); } @@ -273,16 +275,18 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { if (mOverlays == null || !shouldOptimizeVisibility()) { return; } - + Trace.beginSection("ScreenDecorations#updateOverlayWindowVisibilityIfViewExists"); for (final OverlayWindow overlay : mOverlays) { if (overlay == null) { continue; } if (overlay.getView(view.getId()) != null) { overlay.getRootView().setVisibility(getWindowVisibility(overlay, true)); + Trace.endSection(); return; } } + Trace.endSection(); }); } @@ -370,6 +374,7 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { } private void startOnScreenDecorationsThread() { + Trace.beginSection("ScreenDecorations#startOnScreenDecorationsThread"); mWindowManager = mContext.getSystemService(WindowManager.class); mDisplayManager = mContext.getSystemService(DisplayManager.class); mContext.getDisplay().getDisplayInfo(mDisplayInfo); @@ -472,6 +477,7 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { mDisplayManager.registerDisplayListener(mDisplayListener, mHandler); updateConfiguration(); + Trace.endSection(); } @VisibleForTesting @@ -521,6 +527,12 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { } private void setupDecorations() { + Trace.beginSection("ScreenDecorations#setupDecorations"); + setupDecorationsInner(); + Trace.endSection(); + } + + private void setupDecorationsInner() { if (hasRoundedCorners() || shouldDrawCutout() || isPrivacyDotEnabled() || mFaceScanningFactory.getHasProviders()) { @@ -573,7 +585,11 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { return; } - mMainExecutor.execute(() -> mTunerService.addTunable(this, SIZE)); + mMainExecutor.execute(() -> { + Trace.beginSection("ScreenDecorations#addTunable"); + mTunerService.addTunable(this, SIZE); + Trace.endSection(); + }); // Watch color inversion and invert the overlay as needed. if (mColorInversionSetting == null) { @@ -593,7 +609,11 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { mUserTracker.addCallback(mUserChangedCallback, mExecutor); mIsRegistered = true; } else { - mMainExecutor.execute(() -> mTunerService.removeTunable(this)); + mMainExecutor.execute(() -> { + Trace.beginSection("ScreenDecorations#removeTunable"); + mTunerService.removeTunable(this); + Trace.endSection(); + }); if (mColorInversionSetting != null) { mColorInversionSetting.setListening(false); @@ -939,6 +959,7 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { } mExecutor.execute(() -> { + Trace.beginSection("ScreenDecorations#onConfigurationChanged"); int oldRotation = mRotation; mPendingConfigChange = false; updateConfiguration(); @@ -951,6 +972,7 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { // the updated rotation). updateLayoutParams(); } + Trace.endSection(); }); } @@ -1119,6 +1141,7 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { if (mOverlays == null || !SIZE.equals(key)) { return; } + Trace.beginSection("ScreenDecorations#onTuningChanged"); try { final int sizeFactor = Integer.parseInt(newValue); mRoundedCornerResDelegate.setTuningSizeFactor(sizeFactor); @@ -1132,6 +1155,7 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { R.id.rounded_corner_bottom_right }); updateHwLayerRoundedCornerExistAndSize(); + Trace.endSection(); }); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt index e09e65e1b479..17ebdadfa93f 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt @@ -135,7 +135,7 @@ constructor( WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, - WindowManager.LayoutParams.TYPE_SYSTEM_ERROR, + WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG, Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS, PixelFormat.TRANSLUCENT ) diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java index fb37defd844d..63c20651fcd5 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java @@ -301,7 +301,9 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv } else { mView.showDefaultTextPreview(); } - maybeShowRemoteCopy(clipData); + if (!isRemote) { + maybeShowRemoteCopy(clipData); + } animateIn(); mView.announceForAccessibility(accessibilityAnnouncement); if (isRemote) { diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt index 4c8e1ac968f9..a07c716bc8f9 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -28,6 +28,7 @@ import android.content.Intent import android.graphics.drawable.Drawable import android.graphics.drawable.LayerDrawable import android.service.controls.Control +import android.service.controls.ControlsProviderService import android.util.Log import android.view.ContextThemeWrapper import android.view.LayoutInflater @@ -48,6 +49,7 @@ import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.controls.ControlsMetricsLogger import com.android.systemui.controls.ControlsServiceInfo +import com.android.systemui.controls.ControlsSettingsRepository import com.android.systemui.controls.CustomIconCache import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.controller.StructureInfo @@ -96,6 +98,7 @@ class ControlsUiControllerImpl @Inject constructor ( private val userFileManager: UserFileManager, private val userTracker: UserTracker, private val taskViewFactory: Optional<TaskViewFactory>, + private val controlsSettingsRepository: ControlsSettingsRepository, dumpManager: DumpManager ) : ControlsUiController, Dumpable { @@ -354,7 +357,6 @@ class ControlsUiControllerImpl @Inject constructor ( } else { items[0] } - maybeUpdateSelectedItem(selectionItem) createControlsSpaceFrame() @@ -374,11 +376,20 @@ class ControlsUiControllerImpl @Inject constructor ( } private fun createPanelView(componentName: ComponentName) { - val pendingIntent = PendingIntent.getActivity( + val setting = controlsSettingsRepository + .allowActionOnTrivialControlsInLockscreen.value + val pendingIntent = PendingIntent.getActivityAsUser( context, 0, - Intent().setComponent(componentName), - PendingIntent.FLAG_IMMUTABLE + Intent() + .setComponent(componentName) + .putExtra( + ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, + setting + ), + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, + null, + userTracker.userHandle ) parent.requireViewById<View>(R.id.controls_scroll_view).visibility = View.GONE @@ -698,6 +709,8 @@ class ControlsUiControllerImpl @Inject constructor ( println("hidden: $hidden") println("selectedItem: $selectedItem") println("lastSelections: $lastSelections") + println("setting: ${controlsSettingsRepository + .allowActionOnTrivialControlsInLockscreen.value}") } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt index 3012bb41445e..2dd339d409a6 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt @@ -422,6 +422,7 @@ class MediaDataManager( appUid = appUid ) mediaEntries.put(packageName, resumeData) + logSingleVsMultipleMediaAdded(appUid, packageName, instanceId) logger.logResumeMediaAdded(appUid, packageName, instanceId) } backgroundExecutor.execute { @@ -812,6 +813,7 @@ class MediaDataManager( val appUid = appInfo?.uid ?: Process.INVALID_UID if (logEvent) { + logSingleVsMultipleMediaAdded(appUid, sbn.packageName, instanceId) logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation) } else if (playbackLocation != currentEntry?.playbackLocation) { logger.logPlaybackLocationChange(appUid, sbn.packageName, instanceId, playbackLocation) @@ -855,6 +857,20 @@ class MediaDataManager( } } + private fun logSingleVsMultipleMediaAdded( + appUid: Int, + packageName: String, + instanceId: InstanceId + ) { + if (mediaEntries.size == 1) { + logger.logSingleMediaPlayerInCarousel(appUid, packageName, instanceId) + } else if (mediaEntries.size == 2) { + // Since this method is only called when there is a new media session added. + // logging needed once there is more than one media session in carousel. + logger.logMultipleMediaPlayersInCarousel(appUid, packageName, instanceId) + } + } + private fun getAppInfoFromPackage(packageName: String): ApplicationInfo? { try { return context.packageManager.getApplicationInfo(packageName, 0) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt index 3ad8c21e8a1e..ea943be85f21 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt @@ -213,6 +213,24 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) instanceId ) } + + fun logSingleMediaPlayerInCarousel(uid: Int, packageName: String, instanceId: InstanceId) { + logger.logWithInstanceId( + MediaUiEvent.MEDIA_CAROUSEL_SINGLE_PLAYER, + uid, + packageName, + instanceId + ) + } + + fun logMultipleMediaPlayersInCarousel(uid: Int, packageName: String, instanceId: InstanceId) { + logger.logWithInstanceId( + MediaUiEvent.MEDIA_CAROUSEL_MULTIPLE_PLAYERS, + uid, + packageName, + instanceId + ) + } } enum class MediaUiEvent(val metricId: Int) : UiEventLogger.UiEventEnum { @@ -269,7 +287,11 @@ enum class MediaUiEvent(val metricId: Int) : UiEventLogger.UiEventEnum { @UiEvent(doc = "User tapped on a media recommendation card") MEDIA_RECOMMENDATION_CARD_TAP(1045), @UiEvent(doc = "User opened the broadcast dialog from a media control") - MEDIA_OPEN_BROADCAST_DIALOG(1079); + MEDIA_OPEN_BROADCAST_DIALOG(1079), + @UiEvent(doc = "The media carousel contains one media player card") + MEDIA_CAROUSEL_SINGLE_PLAYER(1244), + @UiEvent(doc = "The media carousel contains multiple media player cards") + MEDIA_CAROUSEL_MULTIPLE_PLAYERS(1245); override fun getId() = metricId } diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt index 647beb95a3bc..b10abb569717 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt @@ -48,52 +48,66 @@ class MediaTttCommandLineHelper @Inject constructor( /** All commands for the sender device. */ inner class SenderCommand : Command { override fun execute(pw: PrintWriter, args: List<String>) { - val commandName = args[1] + if (args.size < 2) { + help(pw) + return + } + + val senderArgs = processArgs(args) + @StatusBarManager.MediaTransferSenderState val displayState: Int? try { - displayState = ChipStateSender.getSenderStateIdFromName(commandName) + displayState = ChipStateSender.getSenderStateIdFromName(senderArgs.commandName) } catch (ex: IllegalArgumentException) { - pw.println("Invalid command name $commandName") + pw.println("Invalid command name ${senderArgs.commandName}") return } @SuppressLint("WrongConstant") // sysui allowed to call STATUS_BAR_SERVICE val statusBarManager = context.getSystemService(Context.STATUS_BAR_SERVICE) as StatusBarManager - val routeInfo = MediaRoute2Info.Builder(if (args.size >= 4) args[3] else "id", args[0]) + val routeInfo = MediaRoute2Info.Builder(senderArgs.id, senderArgs.deviceName) .addFeature("feature") - val useAppIcon = !(args.size >= 3 && args[2] == "useAppIcon=false") - if (useAppIcon) { + if (senderArgs.useAppIcon) { routeInfo.setClientPackageName(TEST_PACKAGE_NAME) } + var undoExecutor: Executor? = null + var undoRunnable: Runnable? = null + if (isSucceededState(displayState) && senderArgs.showUndo) { + undoExecutor = mainExecutor + undoRunnable = Runnable { Log.i(CLI_TAG, "Undo triggered for $displayState") } + } + statusBarManager.updateMediaTapToTransferSenderDisplay( displayState, routeInfo.build(), - getUndoExecutor(displayState), - getUndoCallback(displayState) + undoExecutor, + undoRunnable, ) } - private fun getUndoExecutor( - @StatusBarManager.MediaTransferSenderState displayState: Int - ): Executor? { - return if (isSucceededState(displayState)) { - mainExecutor - } else { - null + private fun processArgs(args: List<String>): SenderArgs { + val senderArgs = SenderArgs( + deviceName = args[0], + commandName = args[1], + ) + + if (args.size == 2) { + return senderArgs } - } - private fun getUndoCallback( - @StatusBarManager.MediaTransferSenderState displayState: Int - ): Runnable? { - return if (isSucceededState(displayState)) { - Runnable { Log.i(CLI_TAG, "Undo triggered for $displayState") } - } else { - null + // Process any optional arguments + args.subList(2, args.size).forEach { + when { + it == "useAppIcon=false" -> senderArgs.useAppIcon = false + it == "showUndo=false" -> senderArgs.showUndo = false + it.substring(0, 3) == "id=" -> senderArgs.id = it.substring(3) + } } + + return senderArgs } private fun isSucceededState( @@ -106,14 +120,31 @@ class MediaTttCommandLineHelper @Inject constructor( } override fun help(pw: PrintWriter) { - pw.println("Usage: adb shell cmd statusbar $SENDER_COMMAND " + - "<deviceName> <chipState> useAppIcon=[true|false] <id>") + pw.println( + "Usage: adb shell cmd statusbar $SENDER_COMMAND " + + "<deviceName> <chipState> " + + "useAppIcon=[true|false] id=<id> showUndo=[true|false]" + ) + pw.println("Note: useAppIcon, id, and showUndo are optional additional commands.") } } + private data class SenderArgs( + val deviceName: String, + val commandName: String, + var id: String = "id", + var useAppIcon: Boolean = true, + var showUndo: Boolean = true, + ) + /** All commands for the receiver device. */ inner class ReceiverCommand : Command { override fun execute(pw: PrintWriter, args: List<String>) { + if (args.isEmpty()) { + help(pw) + return + } + val commandName = args[0] @StatusBarManager.MediaTransferReceiverState val displayState: Int? diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java index 3fd1aa73c033..e2f55f0d81b8 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java @@ -145,7 +145,7 @@ public class NavigationBarController implements boolean willApplyConfig = mConfigChanges.applyNewConfig(mContext.getResources()); boolean largeScreenChanged = mIsTablet != isOldConfigTablet; // TODO(b/243765256): Disable this logging once b/243765256 is fixed. - Log.d(DEBUG_MISSING_GESTURE_TAG, "NavbarController: newConfig=" + newConfig + Log.i(DEBUG_MISSING_GESTURE_TAG, "NavbarController: newConfig=" + newConfig + " mTaskbarDelegate initialized=" + mTaskbarDelegate.isInitialized() + " willApplyConfigToNavbars=" + willApplyConfig + " navBarCount=" + mNavigationBars.size()); diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index cb0f3e26c6a7..d03ac3b419f6 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -958,7 +958,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } // TODO(b/243765256): Disable this logging once b/243765256 is fixed. - Log.d(DEBUG_MISSING_GESTURE_TAG, "Config changed: newConfig=" + newConfig + Log.i(DEBUG_MISSING_GESTURE_TAG, "Config changed: newConfig=" + newConfig + " lastReportedConfig=" + mLastReportedConfig); mLastReportedConfig.updateFrom(newConfig); updateDisplaySize(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java index 6240c10a9d5b..cad296b671b3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java @@ -608,7 +608,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P if (TextUtils.isEmpty(tileList)) { tileList = res.getString(R.string.quick_settings_tiles); - if (DEBUG) Log.d(TAG, "Loaded tile specs from config: " + tileList); + if (DEBUG) Log.d(TAG, "Loaded tile specs from default config: " + tileList); } else { if (DEBUG) Log.d(TAG, "Loaded tile specs from setting: " + tileList); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java index e43d4c8a34eb..51de5227b7d4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java @@ -329,7 +329,14 @@ public class InternetTile extends QSTileImpl<SignalState> { mCellularInfo.mAirplaneModeEnabled = icon.visible; mWifiInfo.mAirplaneModeEnabled = icon.visible; if (!mSignalCallback.mEthernetInfo.mConnected) { - if (mWifiInfo.mEnabled && (mWifiInfo.mWifiSignalIconId > 0) + // Always use mWifiInfo to refresh the Internet Tile if airplane mode is enabled, + // because Internet Tile will show different information depending on whether WiFi + // is enabled or not. + if (mWifiInfo.mAirplaneModeEnabled) { + refreshState(mWifiInfo); + // If airplane mode is disabled, we will use mWifiInfo to refresh the Internet Tile + // if WiFi is currently connected to avoid any icon flickering. + } else if (mWifiInfo.mEnabled && (mWifiInfo.mWifiSignalIconId > 0) && (mWifiInfo.mSsid != null)) { refreshState(mWifiInfo); } else { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java index 7130294deccb..a6c7781d891c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java @@ -27,6 +27,7 @@ import android.service.quicksettings.Tile; import android.view.View; import android.widget.Switch; +import androidx.annotation.MainThread; import androidx.annotation.Nullable; import com.android.internal.logging.MetricsLogger; @@ -91,11 +92,13 @@ public class WorkModeTile extends QSTileImpl<BooleanState> implements } @Override + @MainThread public void onManagedProfileChanged() { refreshState(mProfileController.isWorkModeEnabled()); } @Override + @MainThread public void onManagedProfileRemoved() { mHost.removeTile(getTileSpec()); mHost.unmarkTileAsAutoAdded(getTileSpec()); diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index 547b496beaff..00d129ae70ca 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -565,13 +565,25 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis statusBarWinController.registerCallback(mStatusBarWindowCallback); mScreenshotHelper = new ScreenshotHelper(context); - // Listen for tracing state changes commandQueue.addCallback(new CommandQueue.Callbacks() { + + // Listen for tracing state changes @Override public void onTracingStateChanged(boolean enabled) { mSysUiState.setFlag(SYSUI_STATE_TRACING_ENABLED, enabled) .commitUpdate(mContext.getDisplayId()); } + + @Override + public void enterStageSplitFromRunningApp(boolean leftOrTop) { + if (mOverviewProxy != null) { + try { + mOverviewProxy.enterStageSplitFromRunningApp(leftOrTop); + } catch (RemoteException e) { + Log.w(TAG_OPS, "Unable to enter stage split from the current running app"); + } + } + } }); mCommandQueue = commandQueue; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index d000e6e75eed..750d00466a8d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -167,6 +167,7 @@ public class CommandQueue extends IStatusBar.Stub implements private static final int MSG_TILE_SERVICE_REQUEST_LISTENING_STATE = 68 << MSG_SHIFT; private static final int MSG_SHOW_REAR_DISPLAY_DIALOG = 69 << MSG_SHIFT; private static final int MSG_GO_TO_FULLSCREEN_FROM_SPLIT = 70 << MSG_SHIFT; + private static final int MSG_ENTER_STAGE_SPLIT_FROM_RUNNING_APP = 71 << MSG_SHIFT; public static final int FLAG_EXCLUDE_NONE = 0; public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0; @@ -486,6 +487,11 @@ public class CommandQueue extends IStatusBar.Stub implements * @see IStatusBar#goToFullscreenFromSplit */ default void goToFullscreenFromSplit() {} + + /** + * @see IStatusBar#enterStageSplitFromRunningApp + */ + default void enterStageSplitFromRunningApp(boolean leftOrTop) {} } public CommandQueue(Context context) { @@ -1248,6 +1254,14 @@ public class CommandQueue extends IStatusBar.Stub implements } @Override + public void enterStageSplitFromRunningApp(boolean leftOrTop) { + synchronized (mLock) { + mHandler.obtainMessage(MSG_ENTER_STAGE_SPLIT_FROM_RUNNING_APP, + leftOrTop).sendToTarget(); + } + } + + @Override public void requestAddTile( @NonNull ComponentName componentName, @NonNull CharSequence appName, @@ -1758,6 +1772,11 @@ public class CommandQueue extends IStatusBar.Stub implements mCallbacks.get(i).goToFullscreenFromSplit(); } break; + case MSG_ENTER_STAGE_SPLIT_FROM_RUNNING_APP: + for (int i = 0; i < mCallbacks.size(); i++) { + mCallbacks.get(i).enterStageSplitFromRunningApp((Boolean) msg.obj); + } + break; } } } 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 97a47b5d1407..362764d1f89e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java @@ -78,6 +78,7 @@ import com.android.systemui.plugins.log.LogBuffer; import com.android.systemui.plugins.log.LogLevel; import com.android.systemui.qs.tiles.dialog.InternetDialogFactory; import com.android.systemui.settings.UserTracker; +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DataSaverController; import com.android.systemui.statusbar.policy.DataSaverControllerImpl; @@ -193,6 +194,7 @@ public class NetworkControllerImpl extends BroadcastReceiver private final Executor mBgExecutor; // Handler that all callbacks are made on. private final CallbackHandler mCallbackHandler; + private final StatusBarPipelineFlags mStatusBarPipelineFlags; private int mEmergencySource; private boolean mIsEmergency; @@ -243,6 +245,7 @@ public class NetworkControllerImpl extends BroadcastReceiver TelephonyListenerManager telephonyListenerManager, @Nullable WifiManager wifiManager, AccessPointControllerImpl accessPointController, + StatusBarPipelineFlags statusBarPipelineFlags, DemoModeController demoModeController, CarrierConfigTracker carrierConfigTracker, WifiStatusTrackerFactory trackerFactory, @@ -261,6 +264,7 @@ public class NetworkControllerImpl extends BroadcastReceiver bgExecutor, callbackHandler, accessPointController, + statusBarPipelineFlags, new DataUsageController(context), new SubscriptionDefaults(), deviceProvisionedController, @@ -288,6 +292,7 @@ public class NetworkControllerImpl extends BroadcastReceiver Executor bgExecutor, CallbackHandler callbackHandler, AccessPointControllerImpl accessPointController, + StatusBarPipelineFlags statusBarPipelineFlags, DataUsageController dataUsageController, SubscriptionDefaults defaultsHandler, DeviceProvisionedController deviceProvisionedController, @@ -309,6 +314,7 @@ public class NetworkControllerImpl extends BroadcastReceiver mBgLooper = bgLooper; mBgExecutor = bgExecutor; mCallbackHandler = callbackHandler; + mStatusBarPipelineFlags = statusBarPipelineFlags; mDataSaverController = new DataSaverControllerImpl(context); mBroadcastDispatcher = broadcastDispatcher; mMobileFactory = mobileFactory; @@ -1334,7 +1340,7 @@ public class NetworkControllerImpl extends BroadcastReceiver mWifiSignalController.notifyListeners(); } String sims = args.getString("sims"); - if (sims != null) { + if (sims != null && !mStatusBarPipelineFlags.useNewMobileIcons()) { int num = MathUtils.constrain(Integer.parseInt(sims), 1, 8); List<SubscriptionInfo> subs = new ArrayList<>(); if (num != mMobileSignalControllers.size()) { @@ -1357,7 +1363,7 @@ public class NetworkControllerImpl extends BroadcastReceiver mCallbackHandler.setNoSims(mHasNoSubs, mSimDetected); } String mobile = args.getString("mobile"); - if (mobile != null) { + if (mobile != null && !mStatusBarPipelineFlags.useNewMobileIcons()) { boolean show = mobile.equals("show"); String datatype = args.getString("datatype"); String slotString = args.getString("slot"); @@ -1442,7 +1448,7 @@ public class NetworkControllerImpl extends BroadcastReceiver controller.notifyListeners(); } String carrierNetworkChange = args.getString("carriernetworkchange"); - if (carrierNetworkChange != null) { + if (carrierNetworkChange != null && !mStatusBarPipelineFlags.useNewMobileIcons()) { boolean show = carrierNetworkChange.equals("show"); for (int i = 0; i < mMobileSignalControllers.size(); i++) { MobileSignalController controller = mMobileSignalControllers.valueAt(i); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt index c6911b1ce410..a96edf525ebb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt @@ -102,22 +102,27 @@ class LockscreenSmartspaceController @Inject constructor( private var showSensitiveContentForManagedUser = false private var managedUserHandle: UserHandle? = null + // TODO(b/202758428): refactor so that we can test color updates via region samping, similar to + // how we test color updates when theme changes (See testThemeChangeUpdatesTextColor). private val updateFun: UpdateColorCallback = { updateTextColorFromRegionSampler() } var stateChangeListener = object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View) { smartspaceViews.add(v as SmartspaceView) - var regionSampler = RegionSampler( - v, - uiExecutor, - bgExecutor, - regionSamplingEnabled, - updateFun - ) - initializeTextColors(regionSampler) - regionSampler.startRegionSampler() - regionSamplers.put(v, regionSampler) + if (regionSamplingEnabled) { + var regionSampler = RegionSampler( + v, + uiExecutor, + bgExecutor, + regionSamplingEnabled, + updateFun + ) + initializeTextColors(regionSampler) + regionSampler.startRegionSampler() + regionSamplers.put(v, regionSampler) + } + connectSession() updateTextColorFromWallpaper() @@ -127,9 +132,11 @@ class LockscreenSmartspaceController @Inject constructor( override fun onViewDetachedFromWindow(v: View) { smartspaceViews.remove(v as SmartspaceView) - var regionSampler = regionSamplers.getValue(v) - regionSampler.stopRegionSampler() - regionSamplers.remove(v) + if (regionSamplingEnabled) { + var regionSampler = regionSamplers.getValue(v) + regionSampler.stopRegionSampler() + regionSamplers.remove(v) + } if (smartspaceViews.isEmpty()) { disconnect() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileController.java index 4969a1c9a20b..6811bf6cce39 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileController.java @@ -14,6 +14,8 @@ package com.android.systemui.statusbar.phone; +import androidx.annotation.MainThread; + import com.android.systemui.statusbar.phone.ManagedProfileController.Callback; import com.android.systemui.statusbar.policy.CallbackController; @@ -25,8 +27,20 @@ public interface ManagedProfileController extends CallbackController<Callback> { boolean isWorkModeEnabled(); - public interface Callback { + /** + * Callback to get updates about work profile status. + */ + interface Callback { + /** + * Called when managed profile change is detected. This always runs on the main thread. + */ + @MainThread void onManagedProfileChanged(); + + /** + * Called when managed profile removal is detected. This always runs on the main thread. + */ + @MainThread void onManagedProfileRemoved(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java index 4beb87ddae2d..abdf8277e0c9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java @@ -33,31 +33,28 @@ import java.util.concurrent.Executor; import javax.inject.Inject; -/** - */ @SysUISingleton public class ManagedProfileControllerImpl implements ManagedProfileController { private final List<Callback> mCallbacks = new ArrayList<>(); - + private final UserTrackerCallback mUserTrackerCallback = new UserTrackerCallback(); private final Context mContext; private final Executor mMainExecutor; private final UserManager mUserManager; private final UserTracker mUserTracker; private final LinkedList<UserInfo> mProfiles; + private boolean mListening; private int mCurrentUser; - /** - */ @Inject public ManagedProfileControllerImpl(Context context, @Main Executor mainExecutor, - UserTracker userTracker) { + UserTracker userTracker, UserManager userManager) { mContext = context; mMainExecutor = mainExecutor; - mUserManager = UserManager.get(mContext); + mUserManager = userManager; mUserTracker = userTracker; - mProfiles = new LinkedList<UserInfo>(); + mProfiles = new LinkedList<>(); } @Override @@ -100,16 +97,22 @@ public class ManagedProfileControllerImpl implements ManagedProfileController { } } if (mProfiles.size() == 0 && hadProfile && (user == mCurrentUser)) { - for (Callback callback : mCallbacks) { - callback.onManagedProfileRemoved(); - } + mMainExecutor.execute(this::notifyManagedProfileRemoved); } mCurrentUser = user; } } + private void notifyManagedProfileRemoved() { + for (Callback callback : mCallbacks) { + callback.onManagedProfileRemoved(); + } + } + public boolean hasActiveProfile() { - if (!mListening) reloadManagedProfiles(); + if (!mListening || mUserTracker.getUserId() != mCurrentUser) { + reloadManagedProfiles(); + } synchronized (mProfiles) { return mProfiles.size() > 0; } @@ -134,28 +137,28 @@ public class ManagedProfileControllerImpl implements ManagedProfileController { mListening = listening; if (listening) { reloadManagedProfiles(); - mUserTracker.addCallback(mUserChangedCallback, mMainExecutor); + mUserTracker.addCallback(mUserTrackerCallback, mMainExecutor); } else { - mUserTracker.removeCallback(mUserChangedCallback); + mUserTracker.removeCallback(mUserTrackerCallback); } } - private final UserTracker.Callback mUserChangedCallback = - new UserTracker.Callback() { - @Override - public void onUserChanged(int newUser, @NonNull Context userContext) { - reloadManagedProfiles(); - for (Callback callback : mCallbacks) { - callback.onManagedProfileChanged(); - } - } + private final class UserTrackerCallback implements UserTracker.Callback { - @Override - public void onProfilesChanged(@NonNull List<UserInfo> profiles) { - reloadManagedProfiles(); - for (Callback callback : mCallbacks) { - callback.onManagedProfileChanged(); - } - } - }; + @Override + public void onUserChanged(int newUser, @NonNull Context userContext) { + reloadManagedProfiles(); + for (Callback callback : mCallbacks) { + callback.onManagedProfileChanged(); + } + } + + @Override + public void onProfilesChanged(@NonNull List<UserInfo> profiles) { + reloadManagedProfiles(); + for (Callback callback : mCallbacks) { + callback.onManagedProfileChanged(); + } + } + } } 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 fb67f1a1bf50..c350c78913d3 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 @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.dagger +import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.TableLogBufferFactory @@ -24,11 +25,12 @@ import com.android.systemui.statusbar.pipeline.airplane.data.repository.Airplane import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModelImpl import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository -import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepositoryImpl +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileRepositorySwitcher import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepositoryImpl import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxyImpl import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository @@ -40,6 +42,8 @@ import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiIntera import dagger.Binds import dagger.Module import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap @Module abstract class StatusBarPipelineModule { @@ -52,26 +56,28 @@ abstract class StatusBarPipelineModule { @Binds abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository - @Binds - abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository + @Binds abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository @Binds abstract fun wifiInteractor(impl: WifiInteractorImpl): WifiInteractor @Binds abstract fun mobileConnectionsRepository( - impl: MobileConnectionsRepositoryImpl + impl: MobileRepositorySwitcher ): MobileConnectionsRepository - @Binds - abstract fun userSetupRepository(impl: UserSetupRepositoryImpl): UserSetupRepository + @Binds abstract fun userSetupRepository(impl: UserSetupRepositoryImpl): UserSetupRepository - @Binds - abstract fun mobileMappingsProxy(impl: MobileMappingsProxyImpl): MobileMappingsProxy + @Binds abstract fun mobileMappingsProxy(impl: MobileMappingsProxyImpl): MobileMappingsProxy @Binds abstract fun mobileIconsInteractor(impl: MobileIconsInteractorImpl): MobileIconsInteractor + @Binds + @IntoMap + @ClassKey(MobileUiAdapter::class) + abstract fun bindFeature(impl: MobileUiAdapter): CoreStartable + @Module companion object { @JvmStatic diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt index da87f7306e60..5479b92edd22 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt @@ -20,6 +20,7 @@ import android.telephony.TelephonyManager.DATA_CONNECTED import android.telephony.TelephonyManager.DATA_CONNECTING import android.telephony.TelephonyManager.DATA_DISCONNECTED import android.telephony.TelephonyManager.DATA_DISCONNECTING +import android.telephony.TelephonyManager.DATA_UNKNOWN import android.telephony.TelephonyManager.DataState /** Internal enum representation of the telephony data connection states */ @@ -28,6 +29,7 @@ enum class DataConnectionState(@DataState val dataState: Int) { Connecting(DATA_CONNECTING), Disconnected(DATA_DISCONNECTED), Disconnecting(DATA_DISCONNECTING), + Unknown(DATA_UNKNOWN), } fun @receiver:DataState Int.toDataConnectionType(): DataConnectionState = @@ -36,5 +38,6 @@ fun @receiver:DataState Int.toDataConnectionType(): DataConnectionState = DATA_CONNECTING -> DataConnectionState.Connecting DATA_DISCONNECTED -> DataConnectionState.Disconnected DATA_DISCONNECTING -> DataConnectionState.Disconnecting - else -> throw IllegalArgumentException("unknown data state received") + DATA_UNKNOWN -> DataConnectionState.Unknown + else -> throw IllegalArgumentException("unknown data state received $this") } 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 581842bc2f57..f09456342f78 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 @@ -16,44 +16,13 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository -import android.content.Context -import android.database.ContentObserver -import android.provider.Settings.Global -import android.telephony.CellSignalStrength -import android.telephony.CellSignalStrengthCdma -import android.telephony.ServiceState -import android.telephony.SignalStrength import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager import android.telephony.TelephonyCallback -import android.telephony.TelephonyDisplayInfo -import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE import android.telephony.TelephonyManager -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.statusbar.pipeline.mobile.data.model.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel -import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType -import com.android.systemui.statusbar.pipeline.mobile.data.model.toDataConnectionType -import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger -import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange -import com.android.systemui.util.settings.GlobalSettings -import java.lang.IllegalStateException -import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.asExecutor -import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.stateIn /** * Every mobile line of service can be identified via a [SubscriptionInfo] object. We set up a @@ -80,183 +49,3 @@ interface MobileConnectionRepository { */ val isDefaultDataSubscription: StateFlow<Boolean> } - -@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") -@OptIn(ExperimentalCoroutinesApi::class) -class MobileConnectionRepositoryImpl( - private val context: Context, - private val subId: Int, - private val telephonyManager: TelephonyManager, - private val globalSettings: GlobalSettings, - defaultDataSubId: StateFlow<Int>, - globalMobileDataSettingChangedEvent: Flow<Unit>, - bgDispatcher: CoroutineDispatcher, - logger: ConnectivityPipelineLogger, - scope: CoroutineScope, -) : MobileConnectionRepository { - init { - if (telephonyManager.subscriptionId != subId) { - throw IllegalStateException( - "TelephonyManager should be created with subId($subId). " + - "Found ${telephonyManager.subscriptionId} instead." - ) - } - } - - private val telephonyCallbackEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1) - - override val subscriptionModelFlow: StateFlow<MobileSubscriptionModel> = run { - var state = MobileSubscriptionModel() - conflatedCallbackFlow { - // TODO (b/240569788): log all of these into the connectivity logger - val callback = - object : - TelephonyCallback(), - TelephonyCallback.ServiceStateListener, - TelephonyCallback.SignalStrengthsListener, - TelephonyCallback.DataConnectionStateListener, - TelephonyCallback.DataActivityListener, - TelephonyCallback.CarrierNetworkListener, - TelephonyCallback.DisplayInfoListener { - override fun onServiceStateChanged(serviceState: ServiceState) { - state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly) - trySend(state) - } - - override fun onSignalStrengthsChanged(signalStrength: SignalStrength) { - val cdmaLevel = - signalStrength - .getCellSignalStrengths(CellSignalStrengthCdma::class.java) - .let { strengths -> - if (!strengths.isEmpty()) { - strengths[0].level - } else { - CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN - } - } - - val primaryLevel = signalStrength.level - - state = - state.copy( - cdmaLevel = cdmaLevel, - primaryLevel = primaryLevel, - isGsm = signalStrength.isGsm, - ) - trySend(state) - } - - override fun onDataConnectionStateChanged( - dataState: Int, - networkType: Int - ) { - state = - state.copy(dataConnectionState = dataState.toDataConnectionType()) - trySend(state) - } - - override fun onDataActivity(direction: Int) { - state = state.copy(dataActivityDirection = direction) - trySend(state) - } - - override fun onCarrierNetworkChange(active: Boolean) { - state = state.copy(carrierNetworkChangeActive = active) - trySend(state) - } - - override fun onDisplayInfoChanged( - telephonyDisplayInfo: TelephonyDisplayInfo - ) { - val networkType = - if ( - telephonyDisplayInfo.overrideNetworkType == - OVERRIDE_NETWORK_TYPE_NONE - ) { - DefaultNetworkType(telephonyDisplayInfo.networkType) - } else { - OverrideNetworkType(telephonyDisplayInfo.overrideNetworkType) - } - state = state.copy(resolvedNetworkType = networkType) - trySend(state) - } - } - telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) - awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } - } - .onEach { telephonyCallbackEvent.tryEmit(Unit) } - .logOutputChange(logger, "MobileSubscriptionModel") - .stateIn(scope, SharingStarted.WhileSubscribed(), state) - } - - /** Produces whenever the mobile data setting changes for this subId */ - private val localMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow { - val observer = - object : ContentObserver(null) { - override fun onChange(selfChange: Boolean) { - trySend(Unit) - } - } - - globalSettings.registerContentObserver( - globalSettings.getUriFor("${Global.MOBILE_DATA}$subId"), - /* notifyForDescendants */ true, - observer - ) - - awaitClose { context.contentResolver.unregisterContentObserver(observer) } - } - - /** - * There are a few cases where we will need to poll [TelephonyManager] so we can update some - * internal state where callbacks aren't provided. Any of those events should be merged into - * this flow, which can be used to trigger the polling. - */ - private val telephonyPollingEvent: Flow<Unit> = - merge( - telephonyCallbackEvent, - localMobileDataSettingChangedEvent, - globalMobileDataSettingChangedEvent, - ) - - override val dataEnabled: StateFlow<Boolean> = - telephonyPollingEvent - .mapLatest { dataConnectionAllowed() } - .stateIn(scope, SharingStarted.WhileSubscribed(), dataConnectionAllowed()) - - private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed - - override val isDefaultDataSubscription: StateFlow<Boolean> = - defaultDataSubId - .mapLatest { it == subId } - .stateIn(scope, SharingStarted.WhileSubscribed(), defaultDataSubId.value == subId) - - class Factory - @Inject - constructor( - private val context: Context, - private val telephonyManager: TelephonyManager, - private val logger: ConnectivityPipelineLogger, - private val globalSettings: GlobalSettings, - @Background private val bgDispatcher: CoroutineDispatcher, - @Application private val scope: CoroutineScope, - ) { - fun build( - subId: Int, - defaultDataSubId: StateFlow<Int>, - globalMobileDataSettingChangedEvent: Flow<Unit>, - ): MobileConnectionRepository { - return MobileConnectionRepositoryImpl( - context, - subId, - telephonyManager.createForSubscriptionId(subId), - globalSettings, - defaultDataSubId, - globalMobileDataSettingChangedEvent, - bgDispatcher, - logger, - scope, - ) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt index c3c1f1403c60..14200f090c87 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt @@ -16,53 +16,14 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository -import android.annotation.SuppressLint -import android.content.Context -import android.content.IntentFilter -import android.database.ContentObserver -import android.net.ConnectivityManager -import android.net.ConnectivityManager.NetworkCallback -import android.net.Network -import android.net.NetworkCapabilities -import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED -import android.net.NetworkCapabilities.TRANSPORT_CELLULAR import android.provider.Settings -import android.provider.Settings.Global.MOBILE_DATA -import android.telephony.CarrierConfigManager import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager -import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID -import android.telephony.TelephonyCallback -import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener -import android.telephony.TelephonyManager -import androidx.annotation.VisibleForTesting -import com.android.internal.telephony.PhoneConstants import com.android.settingslib.mobile.MobileMappings import com.android.settingslib.mobile.MobileMappings.Config -import com.android.systemui.broadcast.BroadcastDispatcher -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel -import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger -import com.android.systemui.util.settings.GlobalSettings -import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.asExecutor -import kotlinx.coroutines.channels.awaitClose 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 -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.withContext /** * Repo for monitoring the complete active subscription info list, to be consumed and filtered based @@ -90,202 +51,3 @@ interface MobileConnectionsRepository { /** Observe changes to the [Settings.Global.MOBILE_DATA] setting */ val globalMobileDataSettingChangedEvent: Flow<Unit> } - -@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") -@OptIn(ExperimentalCoroutinesApi::class) -@SysUISingleton -class MobileConnectionsRepositoryImpl -@Inject -constructor( - private val connectivityManager: ConnectivityManager, - private val subscriptionManager: SubscriptionManager, - private val telephonyManager: TelephonyManager, - private val logger: ConnectivityPipelineLogger, - broadcastDispatcher: BroadcastDispatcher, - private val globalSettings: GlobalSettings, - private val context: Context, - @Background private val bgDispatcher: CoroutineDispatcher, - @Application private val scope: CoroutineScope, - private val mobileConnectionRepositoryFactory: MobileConnectionRepositoryImpl.Factory -) : MobileConnectionsRepository { - private val subIdRepositoryCache: MutableMap<Int, MobileConnectionRepository> = mutableMapOf() - - /** - * State flow that emits the set of mobile data subscriptions, each represented by its own - * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each - * info object, but for now we keep track of the infos themselves. - */ - override val subscriptionsFlow: StateFlow<List<SubscriptionInfo>> = - conflatedCallbackFlow { - val callback = - object : SubscriptionManager.OnSubscriptionsChangedListener() { - override fun onSubscriptionsChanged() { - trySend(Unit) - } - } - - subscriptionManager.addOnSubscriptionsChangedListener( - bgDispatcher.asExecutor(), - callback, - ) - - awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) } - } - .mapLatest { fetchSubscriptionsList() } - .onEach { infos -> dropUnusedReposFromCache(infos) } - .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf()) - - /** StateFlow that keeps track of the current active mobile data subscription */ - override val activeMobileDataSubscriptionId: StateFlow<Int> = - conflatedCallbackFlow { - val callback = - object : TelephonyCallback(), ActiveDataSubscriptionIdListener { - override fun onActiveDataSubscriptionIdChanged(subId: Int) { - trySend(subId) - } - } - - telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) - awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } - } - .stateIn(scope, started = SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID) - - private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> = - MutableSharedFlow(extraBufferCapacity = 1) - - override val defaultDataSubId: StateFlow<Int> = - broadcastDispatcher - .broadcastFlow( - IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) - ) { intent, _ -> - intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID) - } - .distinctUntilChanged() - .onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) } - .stateIn( - scope, - SharingStarted.WhileSubscribed(), - SubscriptionManager.getDefaultDataSubscriptionId() - ) - - private val carrierConfigChangedEvent = - broadcastDispatcher.broadcastFlow( - IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED) - ) - - /** - * [Config] is an object that tracks relevant configuration flags for a given subscription ID. - * In the case of [MobileMappings], it's hard-coded to check the default data subscription's - * config, so this will apply to every icon that we care about. - * - * Relevant bits in the config are things like - * [CarrierConfigManager.KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL] - * - * This flow will produce whenever the default data subscription or the carrier config changes. - */ - override val defaultDataSubRatConfig: StateFlow<Config> = - merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent) - .mapLatest { Config.readConfig(context) } - .stateIn( - scope, - SharingStarted.WhileSubscribed(), - initialValue = Config.readConfig(context) - ) - - override fun getRepoForSubId(subId: Int): MobileConnectionRepository { - if (!isValidSubId(subId)) { - throw IllegalArgumentException( - "subscriptionId $subId is not in the list of valid subscriptions" - ) - } - - return subIdRepositoryCache[subId] - ?: createRepositoryForSubId(subId).also { subIdRepositoryCache[subId] = it } - } - - /** - * 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) - } - } - - globalSettings.registerContentObserver( - globalSettings.getUriFor(MOBILE_DATA), - true, - observer - ) - - awaitClose { context.contentResolver.unregisterContentObserver(observer) } - } - - @SuppressLint("MissingPermission") - override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> = - conflatedCallbackFlow { - val callback = - object : NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { - override fun onLost(network: Network) { - // Send a disconnected model when lost. Maybe should create a sealed - // type or null here? - trySend(MobileConnectivityModel()) - } - - override fun onCapabilitiesChanged( - network: Network, - caps: NetworkCapabilities - ) { - trySend( - MobileConnectivityModel( - isConnected = caps.hasTransport(TRANSPORT_CELLULAR), - isValidated = caps.hasCapability(NET_CAPABILITY_VALIDATED), - ) - ) - } - } - - connectivityManager.registerDefaultNetworkCallback(callback) - - awaitClose { connectivityManager.unregisterNetworkCallback(callback) } - } - .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectivityModel()) - - private fun isValidSubId(subId: Int): Boolean { - subscriptionsFlow.value.forEach { - if (it.subscriptionId == subId) { - return true - } - } - - return false - } - - @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache - - private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository { - return mobileConnectionRepositoryFactory.build( - subId, - defaultDataSubId, - globalMobileDataSettingChangedEvent, - ) - } - - private fun dropUnusedReposFromCache(newInfos: List<SubscriptionInfo>) { - // Remove any connection repository from the cache that isn't in the new set of IDs. They - // will get garbage collected once their subscribers go away - val currentValidSubscriptionIds = newInfos.map { it.subscriptionId } - - subIdRepositoryCache.keys.forEach { - if (!currentValidSubscriptionIds.contains(it)) { - subIdRepositoryCache.remove(it) - } - } - } - - private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> = - withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt new file mode 100644 index 000000000000..e21400525f00 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt @@ -0,0 +1,162 @@ +/* + * 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.repository + +import android.os.Bundle +import android.telephony.SubscriptionInfo +import androidx.annotation.VisibleForTesting +import com.android.settingslib.mobile.MobileMappings +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.mobile.data.model.MobileConnectivityModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryImpl +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +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 + +/** + * A provider for the [MobileConnectionsRepository] interface that can choose between the Demo and + * Prod concrete implementations at runtime. It works by defining a base flow, [activeRepo], which + * switches based on the latest information from [DemoModeController], and switches every flow in + * the interface to point to the currently-active provider. This allows us to put the demo mode + * interface in its own repository, completely separate from the real version, while still using all + * of the prod implementations for the rest of the pipeline (interactors and onward). Looks + * something like this: + * + * ``` + * RealRepository + * │ + * ├──►RepositorySwitcher──►RealInteractor──►RealViewModel + * │ + * DemoRepository + * ``` + * + * NOTE: because the UI layer for mobile icons relies on a nested-repository structure, it is likely + * that we will have to drain the subscription list whenever demo mode changes. Otherwise if a real + * subscription list [1] is replaced with a demo subscription list [1], the view models will not see + * a change (due to `distinctUntilChanged`) and will not refresh their data providers to the demo + * implementation. + */ +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +class MobileRepositorySwitcher +@Inject +constructor( + @Application scope: CoroutineScope, + val realRepository: MobileConnectionsRepositoryImpl, + val demoMobileConnectionsRepository: DemoMobileConnectionsRepository, + demoModeController: DemoModeController, +) : MobileConnectionsRepository { + + val isDemoMode: StateFlow<Boolean> = + conflatedCallbackFlow { + val callback = + object : DemoMode { + override fun dispatchDemoCommand(command: String?, args: Bundle?) { + // Nothing, we just care about on/off + } + + override fun onDemoModeStarted() { + demoMobileConnectionsRepository.startProcessingCommands() + trySend(true) + } + + override fun onDemoModeFinished() { + demoMobileConnectionsRepository.stopProcessingCommands() + trySend(false) + } + } + + demoModeController.addCallback(callback) + awaitClose { demoModeController.removeCallback(callback) } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), demoModeController.isInDemoMode) + + // Convenient definition flow for the currently active repo (based on demo mode or not) + @VisibleForTesting + internal val activeRepo: StateFlow<MobileConnectionsRepository> = + isDemoMode + .mapLatest { demoMode -> + if (demoMode) { + demoMobileConnectionsRepository + } else { + realRepository + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository) + + override val subscriptionsFlow: StateFlow<List<SubscriptionInfo>> = + activeRepo + .flatMapLatest { it.subscriptionsFlow } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + realRepository.subscriptionsFlow.value + ) + + override val activeMobileDataSubscriptionId: StateFlow<Int> = + activeRepo + .flatMapLatest { it.activeMobileDataSubscriptionId } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + realRepository.activeMobileDataSubscriptionId.value + ) + + override val defaultDataSubRatConfig: StateFlow<MobileMappings.Config> = + activeRepo + .flatMapLatest { it.defaultDataSubRatConfig } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + realRepository.defaultDataSubRatConfig.value + ) + + override val defaultDataSubId: StateFlow<Int> = + activeRepo + .flatMapLatest { it.defaultDataSubId } + .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.defaultDataSubId.value) + + override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> = + activeRepo + .flatMapLatest { it.defaultMobileNetworkConnectivity } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + realRepository.defaultMobileNetworkConnectivity.value + ) + + override val globalMobileDataSettingChangedEvent: Flow<Unit> = + activeRepo.flatMapLatest { it.globalMobileDataSettingChangedEvent } + + override fun getRepoForSubId(subId: Int): MobileConnectionRepository { + if (isDemoMode.value) { + return demoMobileConnectionsRepository.getRepoForSubId(subId) + } + return realRepository.getRepoForSubId(subId) + } +} 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 new file mode 100644 index 000000000000..5f2feb26b739 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt @@ -0,0 +1,277 @@ +/* + * 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.repository.demo + +import android.content.Context +import android.telephony.Annotation +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID +import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_ADVANCED +import android.telephony.TelephonyManager.NETWORK_TYPE_GSM +import android.telephony.TelephonyManager.NETWORK_TYPE_LTE +import android.telephony.TelephonyManager.NETWORK_TYPE_NR +import android.telephony.TelephonyManager.NETWORK_TYPE_UMTS +import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN +import android.util.Log +import com.android.settingslib.SignalIcon +import com.android.settingslib.mobile.MobileMappings +import com.android.settingslib.mobile.TelephonyIcons +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState +import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile +import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +/** This repository vends out data based on demo mode commands */ +@OptIn(ExperimentalCoroutinesApi::class) +class DemoMobileConnectionsRepository +@Inject +constructor( + private val dataSource: DemoModeMobileConnectionDataSource, + @Application private val scope: CoroutineScope, + context: Context, +) : MobileConnectionsRepository { + + private var demoCommandJob: Job? = null + + private val connectionRepoCache = mutableMapOf<Int, DemoMobileConnectionRepository>() + private val subscriptionInfoCache = mutableMapOf<Int, SubscriptionInfo>() + val demoModeFinishedEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1) + + private val _subscriptions = MutableStateFlow<List<SubscriptionInfo>>(listOf()) + override val subscriptionsFlow = + _subscriptions + .onEach { infos -> dropUnusedReposFromCache(infos) } + .stateIn(scope, SharingStarted.WhileSubscribed(), _subscriptions.value) + + private fun dropUnusedReposFromCache(newInfos: List<SubscriptionInfo>) { + // Remove any connection repository from the cache that isn't in the new set of IDs. They + // will get garbage collected once their subscribers go away + val currentValidSubscriptionIds = newInfos.map { it.subscriptionId } + + connectionRepoCache.keys.forEach { + if (!currentValidSubscriptionIds.contains(it)) { + connectionRepoCache.remove(it) + } + } + } + + private fun maybeCreateSubscription(subId: Int) { + if (!subscriptionInfoCache.containsKey(subId)) { + createSubscriptionForSubId(subId, subId).also { subscriptionInfoCache[subId] = it } + + _subscriptions.value = subscriptionInfoCache.values.toList() + } + } + + /** Mimics the old NetworkControllerImpl for now */ + private fun createSubscriptionForSubId(subId: Int, slotIndex: Int): SubscriptionInfo { + return SubscriptionInfo( + subId, + "", + slotIndex, + "", + "", + 0, + 0, + "", + 0, + null, + null, + null, + "", + false, + null, + null, + ) + } + + // TODO(b/261029387): add a command for this value + override val activeMobileDataSubscriptionId = + subscriptionsFlow + .mapLatest { infos -> + // For now, active is just the first in the list + infos.firstOrNull()?.subscriptionId ?: INVALID_SUBSCRIPTION_ID + } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + subscriptionsFlow.value.firstOrNull()?.subscriptionId ?: INVALID_SUBSCRIPTION_ID + ) + + /** Demo mode doesn't currently support modifications to the mobile mappings */ + override val defaultDataSubRatConfig = + MutableStateFlow(MobileMappings.Config.readConfig(context)) + + // TODO(b/261029387): add a command for this value + override val defaultDataSubId = + activeMobileDataSubscriptionId.stateIn( + scope, + SharingStarted.WhileSubscribed(), + INVALID_SUBSCRIPTION_ID + ) + + // TODO(b/261029387): not yet supported + override val defaultMobileNetworkConnectivity = MutableStateFlow(MobileConnectivityModel()) + + override fun getRepoForSubId(subId: Int): DemoMobileConnectionRepository { + return connectionRepoCache[subId] + ?: DemoMobileConnectionRepository(subId).also { connectionRepoCache[subId] = it } + } + + override val globalMobileDataSettingChangedEvent = MutableStateFlow(Unit) + + fun startProcessingCommands() { + demoCommandJob = + scope.launch { + dataSource.mobileEvents.filterNotNull().collect { event -> processEvent(event) } + } + } + + fun stopProcessingCommands() { + demoCommandJob?.cancel() + _subscriptions.value = listOf() + connectionRepoCache.clear() + subscriptionInfoCache.clear() + } + + private fun processEvent(event: FakeNetworkEventModel) { + when (event) { + is Mobile -> { + processEnabledMobileState(event) + } + is MobileDisabled -> { + processDisabledMobileState(event) + } + } + } + + private fun processEnabledMobileState(state: Mobile) { + // get or create the connection repo, and set its values + val subId = state.subId ?: DEFAULT_SUB_ID + maybeCreateSubscription(subId) + + val connection = getRepoForSubId(subId) + // This is always true here, because we split out disabled states at the data-source level + connection.dataEnabled.value = true + connection.isDefaultDataSubscription.value = state.dataType != null + + connection.subscriptionModelFlow.value = state.toMobileSubscriptionModel() + } + + private fun processDisabledMobileState(state: MobileDisabled) { + if (_subscriptions.value.isEmpty()) { + // Nothing to do here + return + } + + val subId = + state.subId + ?: run { + // For sake of usability, we can allow for no subId arg if there is only one + // subscription + if (_subscriptions.value.size > 1) { + Log.d( + TAG, + "processDisabledMobileState: Unable to infer subscription to " + + "disable. Specify subId using '-e slot <subId>'" + + "Known subIds: [${subIdsString()}]" + ) + return + } + + // Use the only existing subscription as our arg, since there is only one + _subscriptions.value[0].subscriptionId + } + + removeSubscription(subId) + } + + private fun removeSubscription(subId: Int) { + val currentSubscriptions = _subscriptions.value + subscriptionInfoCache.remove(subId) + _subscriptions.value = currentSubscriptions.filter { it.subscriptionId != subId } + } + + private fun subIdsString(): String = + _subscriptions.value.joinToString(",") { it.subscriptionId.toString() } + + companion object { + private const val TAG = "DemoMobileConnectionsRepo" + + private const val DEFAULT_SUB_ID = 1 + } +} + +private fun Mobile.toMobileSubscriptionModel(): MobileSubscriptionModel { + return MobileSubscriptionModel( + isEmergencyOnly = false, // TODO(b/261029387): not yet supported + isGsm = false, // TODO(b/261029387): not yet supported + cdmaLevel = level ?: 0, + primaryLevel = level ?: 0, + dataConnectionState = DataConnectionState.Connected, // TODO(b/261029387): not yet supported + dataActivityDirection = activity, + carrierNetworkChangeActive = carrierNetworkChange, + // TODO(b/261185097): once mobile mappings can be mocked at this layer, we can build our + // own demo map + resolvedNetworkType = dataType.toResolvedNetworkType() + ) +} + +@Annotation.NetworkType +private fun SignalIcon.MobileIconGroup?.toNetworkType(): Int = + when (this) { + TelephonyIcons.THREE_G -> NETWORK_TYPE_GSM + TelephonyIcons.LTE -> NETWORK_TYPE_LTE + TelephonyIcons.FOUR_G -> NETWORK_TYPE_UMTS + TelephonyIcons.NR_5G -> NETWORK_TYPE_NR + TelephonyIcons.NR_5G_PLUS -> OVERRIDE_NETWORK_TYPE_NR_ADVANCED + else -> NETWORK_TYPE_UNKNOWN + } + +private fun SignalIcon.MobileIconGroup?.toResolvedNetworkType(): ResolvedNetworkType = + when (this) { + TelephonyIcons.NR_5G_PLUS -> OverrideNetworkType(toNetworkType()) + else -> DefaultNetworkType(toNetworkType()) + } + +class DemoMobileConnectionRepository(val subId: Int) : MobileConnectionRepository { + override val subscriptionModelFlow = MutableStateFlow(MobileSubscriptionModel()) + + override val dataEnabled = MutableStateFlow(true) + + override val isDefaultDataSubscription = 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 new file mode 100644 index 000000000000..da55787e8fed --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt @@ -0,0 +1,141 @@ +/* + * 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.repository.demo + +import android.os.Bundle +import android.telephony.Annotation.DataActivityType +import android.telephony.TelephonyManager.DATA_ACTIVITY_IN +import android.telephony.TelephonyManager.DATA_ACTIVITY_INOUT +import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE +import android.telephony.TelephonyManager.DATA_ACTIVITY_OUT +import 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 +import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile +import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled +import 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 + +/** + * Data source that can map from demo mode commands to inputs into the + * [DemoMobileConnectionsRepository]'s flows + */ +@SysUISingleton +class DemoModeMobileConnectionDataSource +@Inject +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) } + } + + // 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 + private val _mobileCommands = demoCommandStream.map { args -> args.toMobileEvent() } + val mobileEvents = _mobileCommands.shareIn(scope, SharingStarted.WhileSubscribed()) + + private fun Bundle.toMobileEvent(): FakeNetworkEventModel? { + val mobile = getString("mobile") ?: return null + return if (mobile == "show") { + activeMobileEvent() + } else { + MobileDisabled(subId = getString("slot")?.toInt()) + } + } + + /** Parse a valid mobile command string into a network event */ + private fun Bundle.activeMobileEvent(): Mobile { + // There are many key/value pairs supported by mobile demo mode. Bear with me here + val level = getString("level")?.toInt() + val dataType = getString("datatype")?.toDataType() + val slot = getString("slot")?.toInt() + val carrierId = getString("carrierid")?.toInt() + val inflateStrength = getString("inflate")?.toBoolean() + val activity = getString("activity")?.toActivity() + val carrierNetworkChange = getString("carriernetworkchange") == "show" + + return Mobile( + level = level, + dataType = dataType, + subId = slot, + carrierId = carrierId, + inflateStrength = inflateStrength, + activity = activity, + carrierNetworkChange = carrierNetworkChange, + ) + } +} + +private fun String.toDataType(): MobileIconGroup = + when (this) { + "1x" -> TelephonyIcons.ONE_X + "3g" -> TelephonyIcons.THREE_G + "4g" -> TelephonyIcons.FOUR_G + "4g+" -> TelephonyIcons.FOUR_G_PLUS + "5g" -> TelephonyIcons.NR_5G + "5ge" -> TelephonyIcons.LTE_CA_5G_E + "5g+" -> TelephonyIcons.NR_5G_PLUS + "e" -> TelephonyIcons.E + "g" -> TelephonyIcons.G + "h" -> TelephonyIcons.H + "h+" -> TelephonyIcons.H_PLUS + "lte" -> TelephonyIcons.LTE + "lte+" -> TelephonyIcons.LTE_PLUS + "dis" -> TelephonyIcons.DATA_DISABLED + "not" -> TelephonyIcons.NOT_DEFAULT_DATA + else -> TelephonyIcons.UNKNOWN + } + +@DataActivityType +private fun String.toActivity(): Int = + when (this) { + "inout" -> DATA_ACTIVITY_INOUT + "in" -> DATA_ACTIVITY_IN + "out" -> DATA_ACTIVITY_OUT + else -> DATA_ACTIVITY_NONE + } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt new file mode 100644 index 000000000000..3f3acafd2d1c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt @@ -0,0 +1,43 @@ +/* + * 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.repository.demo.model + +import android.telephony.Annotation.DataActivityType +import com.android.settingslib.SignalIcon + +/** + * Model for the demo commands, ported from [NetworkControllerImpl] + * + * Nullable fields represent optional command line arguments + */ +sealed interface FakeNetworkEventModel { + data class Mobile( + val level: Int?, + val dataType: SignalIcon.MobileIconGroup?, + // Null means the default (chosen by the repository) + val subId: Int?, + val carrierId: Int?, + val inflateStrength: Boolean?, + @DataActivityType val activity: Int?, + val carrierNetworkChange: Boolean, + ) : FakeNetworkEventModel + + data class MobileDisabled( + // Null means the default (chosen by the repository) + val subId: Int? + ) : FakeNetworkEventModel +} 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 new file mode 100644 index 000000000000..4c1cf4a3ed08 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt @@ -0,0 +1,235 @@ +/* + * 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.repository.prod + +import android.content.Context +import android.database.ContentObserver +import android.provider.Settings.Global +import android.telephony.CellSignalStrength +import android.telephony.CellSignalStrengthCdma +import android.telephony.ServiceState +import android.telephony.SignalStrength +import android.telephony.TelephonyCallback +import android.telephony.TelephonyDisplayInfo +import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE +import android.telephony.TelephonyManager +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.statusbar.pipeline.mobile.data.model.DefaultNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.model.toDataConnectionType +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange +import com.android.systemui.util.settings.GlobalSettings +import java.lang.IllegalStateException +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +class MobileConnectionRepositoryImpl( + private val context: Context, + private val subId: Int, + private val telephonyManager: TelephonyManager, + private val globalSettings: GlobalSettings, + defaultDataSubId: StateFlow<Int>, + globalMobileDataSettingChangedEvent: Flow<Unit>, + bgDispatcher: CoroutineDispatcher, + logger: ConnectivityPipelineLogger, + scope: CoroutineScope, +) : MobileConnectionRepository { + init { + if (telephonyManager.subscriptionId != subId) { + throw IllegalStateException( + "TelephonyManager should be created with subId($subId). " + + "Found ${telephonyManager.subscriptionId} instead." + ) + } + } + + private val telephonyCallbackEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1) + + override val subscriptionModelFlow: StateFlow<MobileSubscriptionModel> = run { + var state = MobileSubscriptionModel() + conflatedCallbackFlow { + // TODO (b/240569788): log all of these into the connectivity logger + val callback = + object : + TelephonyCallback(), + TelephonyCallback.ServiceStateListener, + TelephonyCallback.SignalStrengthsListener, + TelephonyCallback.DataConnectionStateListener, + TelephonyCallback.DataActivityListener, + TelephonyCallback.CarrierNetworkListener, + TelephonyCallback.DisplayInfoListener { + override fun onServiceStateChanged(serviceState: ServiceState) { + state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly) + trySend(state) + } + + override fun onSignalStrengthsChanged(signalStrength: SignalStrength) { + val cdmaLevel = + signalStrength + .getCellSignalStrengths(CellSignalStrengthCdma::class.java) + .let { strengths -> + if (!strengths.isEmpty()) { + strengths[0].level + } else { + CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN + } + } + + val primaryLevel = signalStrength.level + + state = + state.copy( + cdmaLevel = cdmaLevel, + primaryLevel = primaryLevel, + isGsm = signalStrength.isGsm, + ) + trySend(state) + } + + override fun onDataConnectionStateChanged( + dataState: Int, + networkType: Int + ) { + state = + state.copy(dataConnectionState = dataState.toDataConnectionType()) + trySend(state) + } + + override fun onDataActivity(direction: Int) { + state = state.copy(dataActivityDirection = direction) + trySend(state) + } + + override fun onCarrierNetworkChange(active: Boolean) { + state = state.copy(carrierNetworkChangeActive = active) + trySend(state) + } + + override fun onDisplayInfoChanged( + telephonyDisplayInfo: TelephonyDisplayInfo + ) { + val networkType = + if ( + telephonyDisplayInfo.overrideNetworkType == + OVERRIDE_NETWORK_TYPE_NONE + ) { + DefaultNetworkType(telephonyDisplayInfo.networkType) + } else { + OverrideNetworkType(telephonyDisplayInfo.overrideNetworkType) + } + state = state.copy(resolvedNetworkType = networkType) + trySend(state) + } + } + telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) + awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } + } + .onEach { telephonyCallbackEvent.tryEmit(Unit) } + .logOutputChange(logger, "MobileSubscriptionModel") + .stateIn(scope, SharingStarted.WhileSubscribed(), state) + } + + /** Produces whenever the mobile data setting changes for this subId */ + private val localMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow { + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(Unit) + } + } + + globalSettings.registerContentObserver( + globalSettings.getUriFor("${Global.MOBILE_DATA}$subId"), + /* notifyForDescendants */ true, + observer + ) + + awaitClose { context.contentResolver.unregisterContentObserver(observer) } + } + + /** + * There are a few cases where we will need to poll [TelephonyManager] so we can update some + * internal state where callbacks aren't provided. Any of those events should be merged into + * this flow, which can be used to trigger the polling. + */ + private val telephonyPollingEvent: Flow<Unit> = + merge( + telephonyCallbackEvent, + localMobileDataSettingChangedEvent, + globalMobileDataSettingChangedEvent, + ) + + override val dataEnabled: StateFlow<Boolean> = + telephonyPollingEvent + .mapLatest { dataConnectionAllowed() } + .stateIn(scope, SharingStarted.WhileSubscribed(), dataConnectionAllowed()) + + private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed + + override val isDefaultDataSubscription: StateFlow<Boolean> = + defaultDataSubId + .mapLatest { it == subId } + .stateIn(scope, SharingStarted.WhileSubscribed(), defaultDataSubId.value == subId) + + class Factory + @Inject + constructor( + private val context: Context, + private val telephonyManager: TelephonyManager, + private val logger: ConnectivityPipelineLogger, + private val globalSettings: GlobalSettings, + @Background private val bgDispatcher: CoroutineDispatcher, + @Application private val scope: CoroutineScope, + ) { + fun build( + subId: Int, + defaultDataSubId: StateFlow<Int>, + globalMobileDataSettingChangedEvent: Flow<Unit>, + ): MobileConnectionRepository { + return MobileConnectionRepositoryImpl( + context, + subId, + telephonyManager.createForSubscriptionId(subId), + globalSettings, + defaultDataSubId, + globalMobileDataSettingChangedEvent, + bgDispatcher, + logger, + scope, + ) + } + } +} 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 new file mode 100644 index 000000000000..08d6010f0d99 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt @@ -0,0 +1,265 @@ +/* + * 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.repository.prod + +import android.annotation.SuppressLint +import android.content.Context +import android.content.IntentFilter +import android.database.ContentObserver +import android.net.ConnectivityManager +import android.net.ConnectivityManager.NetworkCallback +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED +import android.net.NetworkCapabilities.TRANSPORT_CELLULAR +import android.provider.Settings.Global.MOBILE_DATA +import android.telephony.CarrierConfigManager +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID +import android.telephony.TelephonyCallback +import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener +import android.telephony.TelephonyManager +import androidx.annotation.VisibleForTesting +import com.android.internal.telephony.PhoneConstants +import com.android.settingslib.mobile.MobileMappings +import com.android.settingslib.mobile.MobileMappings.Config +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.util.settings.GlobalSettings +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.channels.awaitClose +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 +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class MobileConnectionsRepositoryImpl +@Inject +constructor( + private val connectivityManager: ConnectivityManager, + private val subscriptionManager: SubscriptionManager, + private val telephonyManager: TelephonyManager, + private val logger: ConnectivityPipelineLogger, + broadcastDispatcher: BroadcastDispatcher, + private val globalSettings: GlobalSettings, + private val context: Context, + @Background private val bgDispatcher: CoroutineDispatcher, + @Application private val scope: CoroutineScope, + private val mobileConnectionRepositoryFactory: MobileConnectionRepositoryImpl.Factory +) : MobileConnectionsRepository { + private val subIdRepositoryCache: MutableMap<Int, MobileConnectionRepository> = mutableMapOf() + + /** + * State flow that emits the set of mobile data subscriptions, each represented by its own + * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each + * info object, but for now we keep track of the infos themselves. + */ + override val subscriptionsFlow: StateFlow<List<SubscriptionInfo>> = + conflatedCallbackFlow { + val callback = + object : SubscriptionManager.OnSubscriptionsChangedListener() { + override fun onSubscriptionsChanged() { + trySend(Unit) + } + } + + subscriptionManager.addOnSubscriptionsChangedListener( + bgDispatcher.asExecutor(), + callback, + ) + + awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) } + } + .mapLatest { fetchSubscriptionsList() } + .onEach { infos -> dropUnusedReposFromCache(infos) } + .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf()) + + /** StateFlow that keeps track of the current active mobile data subscription */ + override val activeMobileDataSubscriptionId: StateFlow<Int> = + conflatedCallbackFlow { + val callback = + object : TelephonyCallback(), ActiveDataSubscriptionIdListener { + override fun onActiveDataSubscriptionIdChanged(subId: Int) { + trySend(subId) + } + } + + telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) + awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } + } + .stateIn(scope, started = SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID) + + private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> = + MutableSharedFlow(extraBufferCapacity = 1) + + override val defaultDataSubId: StateFlow<Int> = + broadcastDispatcher + .broadcastFlow( + IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) + ) { intent, _ -> + intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID) + } + .distinctUntilChanged() + .onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + SubscriptionManager.getDefaultDataSubscriptionId() + ) + + private val carrierConfigChangedEvent = + broadcastDispatcher.broadcastFlow( + IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED) + ) + + /** + * [Config] is an object that tracks relevant configuration flags for a given subscription ID. + * In the case of [MobileMappings], it's hard-coded to check the default data subscription's + * config, so this will apply to every icon that we care about. + * + * Relevant bits in the config are things like + * [CarrierConfigManager.KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL] + * + * This flow will produce whenever the default data subscription or the carrier config changes. + */ + override val defaultDataSubRatConfig: StateFlow<Config> = + merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent) + .mapLatest { Config.readConfig(context) } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + initialValue = Config.readConfig(context) + ) + + override fun getRepoForSubId(subId: Int): MobileConnectionRepository { + if (!isValidSubId(subId)) { + throw IllegalArgumentException( + "subscriptionId $subId is not in the list of valid subscriptions" + ) + } + + return subIdRepositoryCache[subId] + ?: createRepositoryForSubId(subId).also { subIdRepositoryCache[subId] = it } + } + + /** + * 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) + } + } + + globalSettings.registerContentObserver( + globalSettings.getUriFor(MOBILE_DATA), + true, + observer + ) + + awaitClose { context.contentResolver.unregisterContentObserver(observer) } + } + + @SuppressLint("MissingPermission") + override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> = + conflatedCallbackFlow { + val callback = + object : NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { + override fun onLost(network: Network) { + // Send a disconnected model when lost. Maybe should create a sealed + // type or null here? + trySend(MobileConnectivityModel()) + } + + override fun onCapabilitiesChanged( + network: Network, + caps: NetworkCapabilities + ) { + trySend( + MobileConnectivityModel( + isConnected = caps.hasTransport(TRANSPORT_CELLULAR), + isValidated = caps.hasCapability(NET_CAPABILITY_VALIDATED), + ) + ) + } + } + + connectivityManager.registerDefaultNetworkCallback(callback) + + awaitClose { connectivityManager.unregisterNetworkCallback(callback) } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectivityModel()) + + private fun isValidSubId(subId: Int): Boolean { + subscriptionsFlow.value.forEach { + if (it.subscriptionId == subId) { + return true + } + } + + return false + } + + @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache + + private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository { + return mobileConnectionRepositoryFactory.build( + subId, + defaultDataSubId, + globalMobileDataSettingChangedEvent, + ) + } + + private fun dropUnusedReposFromCache(newInfos: List<SubscriptionInfo>) { + // Remove any connection repository from the cache that isn't in the new set of IDs. They + // will get garbage collected once their subscribers go away + val currentValidSubscriptionIds = newInfos.map { it.subscriptionId } + + subIdRepositoryCache.keys.forEach { + if (!currentValidSubscriptionIds.contains(it)) { + subIdRepositoryCache.remove(it) + } + } + } + + private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> = + withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList } +} 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 c7e0ce173ece..d9487bf92260 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 @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.mobile.ui +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 @@ -29,9 +30,10 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch /** * This class is intended to provide a context to collect on the @@ -50,9 +52,9 @@ constructor( interactor: MobileIconsInteractor, private val iconController: StatusBarIconController, private val iconsViewModelFactory: MobileIconsViewModel.Factory, - @Application scope: CoroutineScope, + @Application private val scope: CoroutineScope, private val statusBarPipelineFlags: StatusBarPipelineFlags, -) { +) : CoreStartable { private val mobileSubIds: Flow<List<Int>> = interactor.filteredSubscriptions.mapLatest { infos -> infos.map { subscriptionInfo -> subscriptionInfo.subscriptionId } @@ -66,18 +68,19 @@ constructor( * NOTE: this should go away as the view presenter learns more about this data pipeline */ private val mobileSubIdsState: StateFlow<List<Int>> = - mobileSubIds - .onEach { - // Only notify the icon controller if we want to *render* the new icons. - // Note that this flow may still run if - // [statusBarPipelineFlags.runNewMobileIconsBackend] is true because we may want to - // get the logging data without rendering. - if (statusBarPipelineFlags.useNewMobileIcons()) { - // Notify the icon controller here so that it knows to add icons - iconController.setNewMobileIconSubIds(it) - } + mobileSubIds.stateIn(scope, SharingStarted.WhileSubscribed(), listOf()) + + override fun start() { + // Only notify the icon controller if we want to *render* the new icons. + // Note that this flow may still run if + // [statusBarPipelineFlags.runNewMobileIconsBackend] is true because we may want to + // get the logging data without rendering. + if (statusBarPipelineFlags.useNewMobileIcons()) { + scope.launch { + mobileSubIds.collectLatest { iconController.setNewMobileIconSubIds(it) } } - .stateIn(scope, SharingStarted.WhileSubscribed(), listOf()) + } + } /** * Create a MobileIconsViewModel for a given [IconManager], and bind it to to the manager's diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt index 162c915552ed..b2ec27c8ce43 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt @@ -40,6 +40,8 @@ import com.android.systemui.dagger.qualifiers.UiBackground import com.android.systemui.statusbar.LightRevealEffect import com.android.systemui.statusbar.LightRevealScrim import com.android.systemui.statusbar.LinearLightRevealEffect +import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation.AddOverlayReason.FOLD +import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation.AddOverlayReason.UNFOLD import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener import com.android.systemui.unfold.updates.RotationChangeProvider import com.android.systemui.unfold.util.ScaleAwareTransitionProgressProvider.Companion.areAnimationsEnabled @@ -125,7 +127,7 @@ constructor( try { // Add the view only if we are unfolding and this is the first screen on if (!isFolded && !isUnfoldHandled && contentResolver.areAnimationsEnabled()) { - executeInBackground { addView(onOverlayReady) } + executeInBackground { addOverlay(onOverlayReady, reason = UNFOLD) } isUnfoldHandled = true } else { // No unfold transition, immediately report that overlay is ready @@ -137,7 +139,7 @@ constructor( } } - private fun addView(onOverlayReady: Runnable? = null) { + private fun addOverlay(onOverlayReady: Runnable? = null, reason: AddOverlayReason) { if (!::wwm.isInitialized) { // Surface overlay is not created yet on the first SysUI launch onOverlayReady?.run() @@ -152,7 +154,10 @@ constructor( LightRevealScrim(context, null).apply { revealEffect = createLightRevealEffect() isScrimOpaqueChangedListener = Consumer {} - revealAmount = 0f + revealAmount = when (reason) { + FOLD -> TRANSPARENT + UNFOLD -> BLACK + } } val params = getLayoutParams() @@ -228,7 +233,7 @@ constructor( } private fun getUnfoldedDisplayInfo(): DisplayInfo = - displayManager.displays + displayManager.getDisplays(DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED) .asSequence() .map { DisplayInfo().apply { it.getDisplayInfo(this) } } .filter { it.type == Display.TYPE_INTERNAL } @@ -247,7 +252,7 @@ constructor( override fun onTransitionStarted() { // Add view for folding case (when unfolding the view is added earlier) if (scrimView == null) { - executeInBackground { addView() } + executeInBackground { addOverlay(reason = FOLD) } } // Disable input dispatching during transition. InputManager.getInstance().cancelCurrentTouch() @@ -294,11 +299,17 @@ constructor( } ) + private enum class AddOverlayReason { FOLD, UNFOLD } + private companion object { - private const val ROTATION_ANIMATION_OVERLAY_Z_INDEX = Integer.MAX_VALUE + const val ROTATION_ANIMATION_OVERLAY_Z_INDEX = Integer.MAX_VALUE // Put the unfold overlay below the rotation animation screenshot to hide the moment // when it is rotated but the rotation of the other windows hasn't happen yet - private const val UNFOLD_OVERLAY_LAYER_Z_INDEX = ROTATION_ANIMATION_OVERLAY_Z_INDEX - 1 + const val UNFOLD_OVERLAY_LAYER_Z_INDEX = ROTATION_ANIMATION_OVERLAY_Z_INDEX - 1 + + // constants for revealAmount. + const val TRANSPARENT = 1f + const val BLACK = 0f } } diff --git a/packages/SystemUI/animation/tests/com/android/systemui/animation/InterpolatorsAndroidXTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/InterpolatorsAndroidXTest.kt index 389eed015820..2c680be97e95 100644 --- a/packages/SystemUI/animation/tests/com/android/systemui/animation/InterpolatorsAndroidXTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/InterpolatorsAndroidXTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.animation import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase import java.lang.reflect.Modifier import junit.framework.Assert.assertEquals import org.junit.Test @@ -25,7 +26,7 @@ import org.junit.runners.JUnit4 @SmallTest @RunWith(JUnit4::class) -class InterpolatorsAndroidXTest { +class InterpolatorsAndroidXTest : SysuiTestCase() { @Test fun testInterpolatorsAndInterpolatorsAndroidXPublicMethodsAreEqual() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt index e7d5632c7087..3c40835fe59d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt @@ -47,6 +47,7 @@ import android.view.WindowInsets import android.view.WindowManager import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY +import android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG import android.view.WindowMetrics import androidx.test.filters.SmallTest import com.airbnb.lottie.LottieAnimationView @@ -423,6 +424,21 @@ class SideFpsControllerTest : SysuiTestCase() { } @Test + fun testLayoutParams_isKeyguardDialogType() = + testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED) { + sideFpsController.overlayOffsets = sensorLocation + sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds) + overlayController.show(SENSOR_ID, REASON_UNKNOWN) + executor.runAllReady() + + verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture()) + + val lpType = overlayViewParamsCaptor.value.type + + assertThat((lpType and TYPE_KEYGUARD_DIALOG) != 0).isTrue() + } + + @Test fun testLayoutParams_hasNoMoveAnimationWindowFlag() = testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED) { sideFpsController.overlayOffsets = sensorLocation diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt index e679b1391c77..d965e337f47a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt @@ -16,15 +16,26 @@ package com.android.systemui.controls.ui +import android.app.PendingIntent import android.content.ComponentName import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.ServiceInfo +import android.os.UserHandle +import android.service.controls.ControlsProviderService import android.testing.AndroidTestingRunner import android.testing.TestableLooper +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View import android.widget.FrameLayout import androidx.test.filters.SmallTest +import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.controls.ControlsMetricsLogger +import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.CustomIconCache +import com.android.systemui.controls.FakeControlsSettingsRepository import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.controller.StructureInfo import com.android.systemui.controls.management.ControlsListingController @@ -38,19 +49,26 @@ import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock import com.android.systemui.util.time.FakeSystemClock +import com.android.wm.shell.TaskView import com.android.wm.shell.TaskViewFactory import com.google.common.truth.Truth.assertThat import dagger.Lazy import java.util.Optional +import java.util.function.Consumer import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.anyInt import org.mockito.Mockito.anyString -import org.mockito.Mockito.mock +import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.never +import org.mockito.Mockito.spy import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @@ -70,9 +88,9 @@ class ControlsUiControllerImplTest : SysuiTestCase() { @Mock lateinit var userFileManager: UserFileManager @Mock lateinit var userTracker: UserTracker @Mock lateinit var taskViewFactory: TaskViewFactory - @Mock lateinit var activityContext: Context @Mock lateinit var dumpManager: DumpManager val sharedPreferences = FakeSharedPreferences() + lateinit var controlsSettingsRepository: FakeControlsSettingsRepository var uiExecutor = FakeExecutor(FakeSystemClock()) var bgExecutor = FakeExecutor(FakeSystemClock()) @@ -83,6 +101,17 @@ class ControlsUiControllerImplTest : SysuiTestCase() { fun setup() { MockitoAnnotations.initMocks(this) + controlsSettingsRepository = FakeControlsSettingsRepository() + + // This way, it won't be cloned every time `LayoutInflater.fromContext` is called, but we + // need to clone it once so we don't modify the original one. + mContext.addMockSystemService( + Context.LAYOUT_INFLATER_SERVICE, + mContext.baseContext + .getSystemService(LayoutInflater::class.java)!! + .cloneInContext(mContext) + ) + parent = FrameLayout(mContext) underTest = @@ -100,6 +129,7 @@ class ControlsUiControllerImplTest : SysuiTestCase() { userFileManager, userTracker, Optional.of(taskViewFactory), + controlsSettingsRepository, dumpManager ) `when`( @@ -113,11 +143,12 @@ class ControlsUiControllerImplTest : SysuiTestCase() { `when`(userFileManager.getSharedPreferences(anyString(), anyInt(), anyInt())) .thenReturn(sharedPreferences) `when`(userTracker.userId).thenReturn(0) + `when`(userTracker.userHandle).thenReturn(UserHandle.of(0)) } @Test fun testGetPreferredStructure() { - val structureInfo = mock(StructureInfo::class.java) + val structureInfo = mock<StructureInfo>() underTest.getPreferredSelectedItem(listOf(structureInfo)) verify(userFileManager) .getSharedPreferences( @@ -189,14 +220,195 @@ class ControlsUiControllerImplTest : SysuiTestCase() { @Test fun testPanelDoesNotRefreshControls() { val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls")) + setUpPanel(panel) + + underTest.show(parent, {}, context) + verify(controlsController, never()).refreshStatus(any(), any()) + } + + @Test + fun testPanelCallsTaskViewFactoryCreate() { + mockLayoutInflater() + val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls")) + val serviceInfo = setUpPanel(panel) + + underTest.show(parent, {}, context) + + val captor = argumentCaptor<ControlsListingController.ControlsListingCallback>() + + verify(controlsListingController).addCallback(capture(captor)) + + captor.value.onServicesUpdated(listOf(serviceInfo)) + FakeExecutor.exhaustExecutors(uiExecutor, bgExecutor) + + verify(taskViewFactory).create(eq(context), eq(uiExecutor), any()) + } + + @Test + fun testPanelControllerStartActivityWithCorrectArguments() { + mockLayoutInflater() + controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(true) + + val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls")) + val serviceInfo = setUpPanel(panel) + + underTest.show(parent, {}, context) + + val captor = argumentCaptor<ControlsListingController.ControlsListingCallback>() + + verify(controlsListingController).addCallback(capture(captor)) + + captor.value.onServicesUpdated(listOf(serviceInfo)) + FakeExecutor.exhaustExecutors(uiExecutor, bgExecutor) + + val pendingIntent = verifyPanelCreatedAndStartTaskView() + + with(pendingIntent) { + assertThat(isActivity).isTrue() + assertThat(intent.component).isEqualTo(serviceInfo.panelActivity) + assertThat( + intent.getBooleanExtra( + ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, + false + ) + ) + .isTrue() + } + } + + @Test + fun testPendingIntentExtrasAreModified() { + mockLayoutInflater() + controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(true) + + val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls")) + val serviceInfo = setUpPanel(panel) + + underTest.show(parent, {}, context) + + val captor = argumentCaptor<ControlsListingController.ControlsListingCallback>() + + verify(controlsListingController).addCallback(capture(captor)) + + captor.value.onServicesUpdated(listOf(serviceInfo)) + FakeExecutor.exhaustExecutors(uiExecutor, bgExecutor) + + val pendingIntent = verifyPanelCreatedAndStartTaskView() + assertThat( + pendingIntent.intent.getBooleanExtra( + ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, + false + ) + ) + .isTrue() + + underTest.hide() + + clearInvocations(controlsListingController, taskViewFactory) + controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(false) + underTest.show(parent, {}, context) + + verify(controlsListingController).addCallback(capture(captor)) + captor.value.onServicesUpdated(listOf(serviceInfo)) + FakeExecutor.exhaustExecutors(uiExecutor, bgExecutor) + + val newPendingIntent = verifyPanelCreatedAndStartTaskView() + assertThat( + newPendingIntent.intent.getBooleanExtra( + ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, + false + ) + ) + .isFalse() + } + + private fun setUpPanel(panel: SelectedItem.PanelItem): ControlsServiceInfo { + val activity = ComponentName("pkg", "activity") sharedPreferences .edit() .putString("controls_component", panel.componentName.flattenToString()) .putString("controls_structure", panel.appName.toString()) .putBoolean("controls_is_panel", true) .commit() + return ControlsServiceInfo(panel.componentName, panel.appName, activity) + } - underTest.show(parent, {}, activityContext) - verify(controlsController, never()).refreshStatus(any(), any()) + private fun verifyPanelCreatedAndStartTaskView(): PendingIntent { + val taskViewConsumerCaptor = argumentCaptor<Consumer<TaskView>>() + verify(taskViewFactory).create(eq(context), eq(uiExecutor), capture(taskViewConsumerCaptor)) + + val taskView: TaskView = mock { + `when`(this.post(any())).thenAnswer { + uiExecutor.execute(it.arguments[0] as Runnable) + true + } + } + // calls PanelTaskViewController#launchTaskView + taskViewConsumerCaptor.value.accept(taskView) + val listenerCaptor = argumentCaptor<TaskView.Listener>() + verify(taskView).setListener(any(), capture(listenerCaptor)) + listenerCaptor.value.onInitialized() + FakeExecutor.exhaustExecutors(uiExecutor, bgExecutor) + + val pendingIntentCaptor = argumentCaptor<PendingIntent>() + verify(taskView).startActivity(capture(pendingIntentCaptor), any(), any(), any()) + return pendingIntentCaptor.value + } + + private fun ControlsServiceInfo( + componentName: ComponentName, + label: CharSequence, + panelComponentName: ComponentName? = null + ): ControlsServiceInfo { + val serviceInfo = + ServiceInfo().apply { + applicationInfo = ApplicationInfo() + packageName = componentName.packageName + name = componentName.className + } + return spy(ControlsServiceInfo(mContext, serviceInfo)).apply { + `when`(loadLabel()).thenReturn(label) + `when`(loadIcon()).thenReturn(mock()) + `when`(panelActivity).thenReturn(panelComponentName) + } + } + + private fun mockLayoutInflater() { + LayoutInflater.from(context) + .setPrivateFactory( + object : LayoutInflater.Factory2 { + override fun onCreateView( + view: View?, + name: String, + context: Context, + attrs: AttributeSet + ): View? { + return onCreateView(name, context, attrs) + } + + override fun onCreateView( + name: String, + context: Context, + attrs: AttributeSet + ): View? { + if (FrameLayout::class.java.simpleName.equals(name)) { + val mock: FrameLayout = mock { + `when`(this.context).thenReturn(context) + `when`(this.id).thenReturn(R.id.controls_panel) + `when`(this.requireViewById<View>(any())).thenCallRealMethod() + `when`(this.findViewById<View>(R.id.controls_panel)) + .thenReturn(this) + `when`(this.post(any())).thenAnswer { + uiExecutor.execute(it.arguments[0] as Runnable) + true + } + } + return mock + } else { + return null + } + } + } + ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java index d91baa5e7fcb..80c39cf9e4cd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.when; import android.os.Handler; +import android.service.quicksettings.Tile; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -38,6 +39,7 @@ import com.android.systemui.qs.QSTileHost; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tiles.dialog.InternetDialogFactory; import com.android.systemui.statusbar.connectivity.AccessPointController; +import com.android.systemui.statusbar.connectivity.IconState; import com.android.systemui.statusbar.connectivity.NetworkController; import org.junit.Before; @@ -113,4 +115,24 @@ public class InternetTileTest extends SysuiTestCase { .isNotEqualTo(mContext.getString(R.string.quick_settings_networks_available)); assertThat(mTile.getLastTileState()).isEqualTo(-1); } + + @Test + public void setIsAirplaneMode_APM_enabled_wifi_disabled() { + IconState state = new IconState(true, 0, ""); + mTile.mSignalCallback.setIsAirplaneMode(state); + mTestableLooper.processAllMessages(); + assertThat(mTile.getState().state).isEqualTo(Tile.STATE_INACTIVE); + assertThat(mTile.getState().secondaryLabel) + .isEqualTo(mContext.getString(R.string.status_bar_airplane)); + } + + @Test + public void setIsAirplaneMode_APM_enabled_wifi_enabled() { + IconState state = new IconState(false, 0, ""); + mTile.mSignalCallback.setIsAirplaneMode(state); + mTestableLooper.processAllMessages(); + assertThat(mTile.getState().state).isEqualTo(Tile.STATE_ACTIVE); + assertThat(mTile.getState().secondaryLabel) + .isNotEqualTo(mContext.getString(R.string.status_bar_airplane)); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java index faf4592d26e3..5431eba8441c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java @@ -72,6 +72,7 @@ import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dump.DumpManager; import com.android.systemui.plugins.log.LogBuffer; import com.android.systemui.settings.UserTracker; +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener; @@ -245,6 +246,7 @@ public class NetworkControllerBaseTest extends SysuiTestCase { mFakeExecutor, mCallbackHandler, mock(AccessPointControllerImpl.class), + mock(StatusBarPipelineFlags.class), mock(DataUsageController.class), mMockSubDefaults, mMockProvisionController, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java index ca75a40300cb..9441d49d454a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java @@ -49,6 +49,7 @@ import com.android.settingslib.mobile.TelephonyIcons; import com.android.settingslib.net.DataUsageController; import com.android.systemui.dump.DumpManager; import com.android.systemui.plugins.log.LogBuffer; +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.util.CarrierConfigTracker; @@ -150,6 +151,7 @@ public class NetworkControllerDataTest extends NetworkControllerBaseTest { mFakeExecutor, mCallbackHandler, mock(AccessPointControllerImpl.class), + mock(StatusBarPipelineFlags.class), mock(DataUsageController.class), mMockSubDefaults, mock(DeviceProvisionedController.class), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java index 84c242cda459..4c1f0a8a1066 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java @@ -44,6 +44,7 @@ import com.android.settingslib.net.DataUsageController; import com.android.systemui.R; import com.android.systemui.dump.DumpManager; import com.android.systemui.plugins.log.LogBuffer; +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.util.CarrierConfigTracker; @@ -78,6 +79,7 @@ public class NetworkControllerSignalTest extends NetworkControllerBaseTest { mFakeExecutor, mCallbackHandler, mock(AccessPointControllerImpl.class), + mock(StatusBarPipelineFlags.class), mock(DataUsageController.class), mMockSubDefaults, mMockProvisionController, @@ -115,6 +117,7 @@ public class NetworkControllerSignalTest extends NetworkControllerBaseTest { mFakeExecutor, mCallbackHandler, mock(AccessPointControllerImpl.class), + mock(StatusBarPipelineFlags.class), mock(DataUsageController.class), mMockSubDefaults, mMockProvisionController, @@ -150,6 +153,7 @@ public class NetworkControllerSignalTest extends NetworkControllerBaseTest { mFakeExecutor, mCallbackHandler, mock(AccessPointControllerImpl.class), + mock(StatusBarPipelineFlags.class), mock(DataUsageController.class), mMockSubDefaults, mock(DeviceProvisionedController.class), @@ -188,6 +192,7 @@ public class NetworkControllerSignalTest extends NetworkControllerBaseTest { mFakeExecutor, mCallbackHandler, mock(AccessPointControllerImpl.class), + mock(StatusBarPipelineFlags.class), mock(DataUsageController.class), mMockSubDefaults, mock(DeviceProvisionedController.class), @@ -274,6 +279,7 @@ public class NetworkControllerSignalTest extends NetworkControllerBaseTest { mFakeExecutor, mCallbackHandler, mock(AccessPointControllerImpl.class), + mock(StatusBarPipelineFlags.class), mock(DataUsageController.class), mMockSubDefaults, mock(DeviceProvisionedController.class), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImplTest.kt new file mode 100644 index 000000000000..7eba3b463336 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImplTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone + +import android.content.pm.UserInfo +import android.os.UserManager +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.settings.UserTracker +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock +import junit.framework.Assert +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@SmallTest +class ManagedProfileControllerImplTest : SysuiTestCase() { + + private val mainExecutor: FakeExecutor = FakeExecutor(FakeSystemClock()) + + private lateinit var controller: ManagedProfileControllerImpl + + @Mock private lateinit var userTracker: UserTracker + @Mock private lateinit var userManager: UserManager + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + controller = ManagedProfileControllerImpl(context, mainExecutor, userTracker, userManager) + } + + @Test + fun hasWorkingProfile_isWorkModeEnabled_returnsTrue() { + `when`(userTracker.userId).thenReturn(1) + setupWorkingProfile(1) + + Assert.assertEquals(true, controller.hasActiveProfile()) + } + + @Test + fun noWorkingProfile_isWorkModeEnabled_returnsFalse() { + `when`(userTracker.userId).thenReturn(1) + + Assert.assertEquals(false, controller.hasActiveProfile()) + } + + @Test + fun listeningUserChanges_isWorkModeEnabled_returnsTrue() { + `when`(userTracker.userId).thenReturn(1) + controller.addCallback(TestCallback) + `when`(userTracker.userId).thenReturn(2) + setupWorkingProfile(2) + + Assert.assertEquals(true, controller.hasActiveProfile()) + } + + private fun setupWorkingProfile(userId: Int) { + `when`(userManager.getEnabledProfiles(userId)) + .thenReturn( + listOf(UserInfo(userId, "test_user", "", 0, UserManager.USER_TYPE_PROFILE_MANAGED)) + ) + } + + private object TestCallback : ManagedProfileController.Callback { + + override fun onManagedProfileChanged() = Unit + + override fun onManagedProfileRemoved() = Unit + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index 471f8d3404ba..14a319bc87e9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 The Android Open Source Project + * 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. @@ -11,7 +11,7 @@ * 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 + * limitations under the License. */ package com.android.systemui.statusbar.phone; @@ -105,7 +105,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Mock private KeyguardBouncer.Factory mKeyguardBouncerFactory; @Mock private KeyguardMessageAreaController.Factory mKeyguardMessageAreaFactory; @Mock private KeyguardMessageAreaController mKeyguardMessageAreaController; - @Mock private KeyguardBouncer mPrimaryBouncer; @Mock private StatusBarKeyguardViewManager.AlternateBouncer mAlternateBouncer; @Mock private KeyguardMessageArea mKeyguardMessageArea; @Mock private ShadeController mShadeController; @@ -133,16 +132,14 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Before public void setUp() { MockitoAnnotations.initMocks(this); - when(mKeyguardBouncerFactory.create( - any(ViewGroup.class), - any(KeyguardBouncer.PrimaryBouncerExpansionCallback.class))) - .thenReturn(mPrimaryBouncer); when(mCentralSurfaces.getBouncerContainer()).thenReturn(mContainer); when(mContainer.findViewById(anyInt())).thenReturn(mKeyguardMessageArea); when(mKeyguardMessageAreaFactory.create(any(KeyguardMessageArea.class))) .thenReturn(mKeyguardMessageAreaController); when(mBouncerView.getDelegate()).thenReturn(mBouncerViewDelegate); + when(mFeatureFlags.isEnabled(MODERN_BOUNCER)).thenReturn(true); + mStatusBarKeyguardViewManager = new StatusBarKeyguardViewManager( getContext(), @@ -184,7 +181,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mStatusBarKeyguardViewManager.show(null); ArgumentCaptor<KeyguardBouncer.PrimaryBouncerExpansionCallback> callbackArgumentCaptor = ArgumentCaptor.forClass(KeyguardBouncer.PrimaryBouncerExpansionCallback.class); - verify(mKeyguardBouncerFactory).create(any(ViewGroup.class), + verify(mPrimaryBouncerCallbackInteractor).addBouncerExpansionCallback( callbackArgumentCaptor.capture()); mBouncerExpansionCallback = callbackArgumentCaptor.getValue(); } @@ -195,87 +192,87 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { Runnable cancelAction = () -> {}; mStatusBarKeyguardViewManager.dismissWithAction( action, cancelAction, false /* afterKeyguardGone */); - verify(mPrimaryBouncer).showWithDismissAction(eq(action), eq(cancelAction)); + verify(mPrimaryBouncerInteractor).setDismissAction(eq(action), eq(cancelAction)); + verify(mPrimaryBouncerInteractor).show(eq(true)); } @Test public void showBouncer_onlyWhenShowing() { mStatusBarKeyguardViewManager.hide(0 /* startTime */, 0 /* fadeoutDuration */); mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */); - verify(mPrimaryBouncer, never()).show(anyBoolean(), anyBoolean()); - verify(mPrimaryBouncer, never()).show(anyBoolean()); + verify(mPrimaryBouncerInteractor, never()).show(anyBoolean()); } @Test public void showBouncer_notWhenBouncerAlreadyShowing() { mStatusBarKeyguardViewManager.hide(0 /* startTime */, 0 /* fadeoutDuration */); - when(mPrimaryBouncer.isSecure()).thenReturn(true); + when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn( + KeyguardSecurityModel.SecurityMode.Password); mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */); - verify(mPrimaryBouncer, never()).show(anyBoolean(), anyBoolean()); - verify(mPrimaryBouncer, never()).show(anyBoolean()); + verify(mPrimaryBouncerInteractor, never()).show(anyBoolean()); } @Test public void showBouncer_showsTheBouncer() { mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */); - verify(mPrimaryBouncer).show(anyBoolean(), eq(true)); + verify(mPrimaryBouncerInteractor).show(eq(true)); } @Test public void onPanelExpansionChanged_neverShowsDuringHintAnimation() { when(mNotificationPanelView.isUnlockHintRunning()).thenReturn(true); mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT); - verify(mPrimaryBouncer, never()).setExpansion(anyFloat()); + verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat()); } @Test public void onPanelExpansionChanged_propagatesToBouncerOnlyIfShowing() { mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT); - verify(mPrimaryBouncer, never()).setExpansion(eq(0.5f)); + verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(eq(0.5f)); - when(mPrimaryBouncer.isShowing()).thenReturn(true); + when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true); mStatusBarKeyguardViewManager.onPanelExpansionChanged( expansionEvent(/* fraction= */ 0.6f, /* expanded= */ false, /* tracking= */ true)); - verify(mPrimaryBouncer).setExpansion(eq(0.6f)); + verify(mPrimaryBouncerInteractor).setPanelExpansion(eq(0.6f)); } @Test public void onPanelExpansionChanged_duplicateEventsAreIgnored() { - when(mPrimaryBouncer.isShowing()).thenReturn(true); + when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true); mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT); - verify(mPrimaryBouncer).setExpansion(eq(0.5f)); + verify(mPrimaryBouncerInteractor).setPanelExpansion(eq(0.5f)); - reset(mPrimaryBouncer); + reset(mPrimaryBouncerInteractor); mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT); - verify(mPrimaryBouncer, never()).setExpansion(eq(0.5f)); + verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(eq(0.5f)); } @Test public void onPanelExpansionChanged_hideBouncer_afterKeyguardHidden() { mStatusBarKeyguardViewManager.hide(0, 0); - when(mPrimaryBouncer.inTransit()).thenReturn(true); + when(mPrimaryBouncerInteractor.isInTransit()).thenReturn(true); mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT); - verify(mPrimaryBouncer).setExpansion(eq(KeyguardBouncer.EXPANSION_HIDDEN)); + verify(mPrimaryBouncerInteractor).setPanelExpansion(eq(KeyguardBouncer.EXPANSION_HIDDEN)); } @Test public void onPanelExpansionChanged_showsBouncerWhenSwiping() { mKeyguardStateController.setCanDismissLockScreen(false); mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT); - verify(mPrimaryBouncer).show(eq(false), eq(false)); + verify(mPrimaryBouncerInteractor).show(eq(false)); // But not when it's already visible - reset(mPrimaryBouncer); - when(mPrimaryBouncer.isShowing()).thenReturn(true); + reset(mPrimaryBouncerInteractor); + when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true); mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT); - verify(mPrimaryBouncer, never()).show(eq(false), eq(false)); + verify(mPrimaryBouncerInteractor, never()).show(eq(false)); // Or animating away - reset(mPrimaryBouncer); - when(mPrimaryBouncer.isAnimatingAway()).thenReturn(true); + reset(mPrimaryBouncerInteractor); + when(mPrimaryBouncerInteractor.isAnimatingAway()).thenReturn(true); mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT); - verify(mPrimaryBouncer, never()).show(eq(false), eq(false)); + verify(mPrimaryBouncerInteractor, never()).show(eq(false)); } @Test @@ -287,7 +284,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE, /* expanded= */ true, /* tracking= */ false)); - verify(mPrimaryBouncer, never()).setExpansion(anyFloat()); + verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat()); } @Test @@ -304,7 +301,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE, /* expanded= */ true, /* tracking= */ false)); - verify(mPrimaryBouncer, never()).setExpansion(anyFloat()); + verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat()); } @Test @@ -315,7 +312,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE, /* expanded= */ true, /* tracking= */ false)); - verify(mPrimaryBouncer, never()).setExpansion(anyFloat()); + verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat()); } @Test @@ -332,7 +329,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE, /* expanded= */ true, /* tracking= */ false)); - verify(mPrimaryBouncer, never()).setExpansion(anyFloat()); + verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat()); } @Test @@ -343,7 +340,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE, /* expanded= */ true, /* tracking= */ false)); - verify(mPrimaryBouncer, never()).setExpansion(anyFloat()); + verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat()); } @Test @@ -351,7 +348,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, true /* animated */); verify(mCentralSurfaces).animateKeyguardUnoccluding(); - when(mPrimaryBouncer.isShowing()).thenReturn(true); + when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true); clearInvocations(mCentralSurfaces); mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, true /* animated */); verify(mCentralSurfaces, never()).animateKeyguardUnoccluding(); @@ -402,7 +399,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mStatusBarKeyguardViewManager.dismissWithAction( action, cancelAction, true /* afterKeyguardGone */); - when(mPrimaryBouncer.isShowing()).thenReturn(false); + when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false); mStatusBarKeyguardViewManager.hideBouncer(true); mStatusBarKeyguardViewManager.hide(0, 30); verify(action, never()).onDismiss(); @@ -416,7 +413,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mStatusBarKeyguardViewManager.dismissWithAction( action, cancelAction, true /* afterKeyguardGone */); - when(mPrimaryBouncer.isShowing()).thenReturn(false); + when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false); mStatusBarKeyguardViewManager.hideBouncer(true); verify(action, never()).onDismiss(); @@ -438,7 +435,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Test public void testShowing_whenAlternateAuthShowing() { mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); - when(mPrimaryBouncer.isShowing()).thenReturn(false); + when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false); when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true); assertTrue( "Is showing not accurate when alternative auth showing", @@ -448,7 +445,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Test public void testWillBeShowing_whenAlternateAuthShowing() { mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); - when(mPrimaryBouncer.isShowing()).thenReturn(false); + when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false); when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true); assertTrue( "Is or will be showing not accurate when alternative auth showing", @@ -459,7 +456,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { public void testHideAlternateBouncer_onShowBouncer() { // GIVEN alt auth is showing mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); - when(mPrimaryBouncer.isShowing()).thenReturn(false); + when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false); when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true); reset(mAlternateBouncer); @@ -472,8 +469,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Test public void testBouncerIsOrWillBeShowing_whenBouncerIsInTransit() { - when(mPrimaryBouncer.isShowing()).thenReturn(false); - when(mPrimaryBouncer.inTransit()).thenReturn(true); + when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false); + when(mPrimaryBouncerInteractor.isInTransit()).thenReturn(true); assertTrue( "Is or will be showing should be true when bouncer is in transit", @@ -484,7 +481,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { public void testShowAltAuth_unlockingWithBiometricNotAllowed() { // GIVEN alt auth exists, unlocking with biometric isn't allowed mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); - when(mPrimaryBouncer.isShowing()).thenReturn(false); + when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false); when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) .thenReturn(false); @@ -493,7 +490,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mStatusBarKeyguardViewManager.showBouncer(scrimmed); // THEN regular bouncer is shown - verify(mPrimaryBouncer).show(anyBoolean(), eq(scrimmed)); + verify(mPrimaryBouncerInteractor).show(eq(scrimmed)); verify(mAlternateBouncer, never()).showAlternateBouncer(); } @@ -501,7 +498,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { public void testShowAlternateBouncer_unlockingWithBiometricAllowed() { // GIVEN alt auth exists, unlocking with biometric is allowed mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); - when(mPrimaryBouncer.isShowing()).thenReturn(false); + when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false); when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); // WHEN showGenericBouncer is called @@ -509,30 +506,28 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { // THEN alt auth bouncer is shown verify(mAlternateBouncer).showAlternateBouncer(); - verify(mPrimaryBouncer, never()).show(anyBoolean(), anyBoolean()); + verify(mPrimaryBouncerInteractor, never()).show(anyBoolean()); } @Test public void testUpdateResources_delegatesToBouncer() { mStatusBarKeyguardViewManager.updateResources(); - verify(mPrimaryBouncer).updateResources(); + verify(mPrimaryBouncerInteractor).updateResources(); } @Test public void updateKeyguardPosition_delegatesToBouncer() { mStatusBarKeyguardViewManager.updateKeyguardPosition(1.0f); - verify(mPrimaryBouncer).updateKeyguardPosition(1.0f); + verify(mPrimaryBouncerInteractor).setKeyguardPosition(1.0f); } @Test public void testIsBouncerInTransit() { - when(mPrimaryBouncer.inTransit()).thenReturn(true); + when(mPrimaryBouncerInteractor.isInTransit()).thenReturn(true); Truth.assertThat(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).isTrue(); - when(mPrimaryBouncer.inTransit()).thenReturn(false); - Truth.assertThat(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).isFalse(); - mPrimaryBouncer = null; + when(mPrimaryBouncerInteractor.isInTransit()).thenReturn(false); Truth.assertThat(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).isFalse(); } @@ -564,7 +559,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY), mOnBackInvokedCallback.capture()); - when(mPrimaryBouncer.isShowing()).thenReturn(true); + when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true); when(mCentralSurfaces.shouldKeyguardHideImmediately()).thenReturn(true); /* invoke the back callback directly */ mOnBackInvokedCallback.getValue().onBackInvoked(); @@ -594,13 +589,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { } @Test - public void flag_off_DoesNotCallBouncerInteractor() { - when(mFeatureFlags.isEnabled(MODERN_BOUNCER)).thenReturn(false); - mStatusBarKeyguardViewManager.hideBouncer(false); - verify(mPrimaryBouncerInteractor, never()).hide(); - } - - @Test public void hideAlternateBouncer_beforeCentralSurfacesRegistered() { mStatusBarKeyguardViewManager = new StatusBarKeyguardViewManager( diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest_Old.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest_Old.java new file mode 100644 index 000000000000..96fba39d6b59 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest_Old.java @@ -0,0 +1,641 @@ +/* + * Copyright (C) 2018 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.phone; + +import static com.android.systemui.flags.Flags.MODERN_BOUNCER; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewRootImpl; +import android.window.OnBackInvokedCallback; +import android.window.OnBackInvokedDispatcher; +import android.window.WindowOnBackInvokedDispatcher; + +import androidx.test.filters.SmallTest; + +import com.android.internal.util.LatencyTracker; +import com.android.internal.widget.LockPatternUtils; +import com.android.keyguard.KeyguardMessageArea; +import com.android.keyguard.KeyguardMessageAreaController; +import com.android.keyguard.KeyguardSecurityModel; +import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.keyguard.ViewMediatorCallback; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.dock.DockManager; +import com.android.systemui.dreams.DreamOverlayStateController; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.keyguard.data.BouncerView; +import com.android.systemui.keyguard.data.BouncerViewDelegate; +import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor; +import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor; +import com.android.systemui.navigationbar.NavigationModeController; +import com.android.systemui.plugins.ActivityStarter.OnDismissAction; +import com.android.systemui.shade.NotificationPanelViewController; +import com.android.systemui.shade.ShadeController; +import com.android.systemui.shade.ShadeExpansionChangeEvent; +import com.android.systemui.shade.ShadeExpansionStateManager; +import com.android.systemui.statusbar.NotificationMediaManager; +import com.android.systemui.statusbar.NotificationShadeWindowController; +import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.SysuiStatusBarStateController; +import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.systemui.unfold.SysUIUnfoldComponent; + +import com.google.common.truth.Truth; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.util.Optional; + +/** + * StatusBarKeyguardViewManager Test with deprecated KeyguardBouncer.java. + * TODO: Delete when deleting {@link KeyguardBouncer} + */ +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class StatusBarKeyguardViewManagerTest_Old extends SysuiTestCase { + private static final ShadeExpansionChangeEvent EXPANSION_EVENT = + expansionEvent(/* fraction= */ 0.5f, /* expanded= */ false, /* tracking= */ true); + + @Mock private ViewMediatorCallback mViewMediatorCallback; + @Mock private LockPatternUtils mLockPatternUtils; + @Mock private CentralSurfaces mCentralSurfaces; + @Mock private ViewGroup mContainer; + @Mock private NotificationPanelViewController mNotificationPanelView; + @Mock private BiometricUnlockController mBiometricUnlockController; + @Mock private SysuiStatusBarStateController mStatusBarStateController; + @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor; + @Mock private View mNotificationContainer; + @Mock private KeyguardBypassController mBypassController; + @Mock private KeyguardBouncer.Factory mKeyguardBouncerFactory; + @Mock private KeyguardMessageAreaController.Factory mKeyguardMessageAreaFactory; + @Mock private KeyguardMessageAreaController mKeyguardMessageAreaController; + @Mock private KeyguardBouncer mPrimaryBouncer; + @Mock private StatusBarKeyguardViewManager.AlternateBouncer mAlternateBouncer; + @Mock private KeyguardMessageArea mKeyguardMessageArea; + @Mock private ShadeController mShadeController; + @Mock private SysUIUnfoldComponent mSysUiUnfoldComponent; + @Mock private DreamOverlayStateController mDreamOverlayStateController; + @Mock private LatencyTracker mLatencyTracker; + @Mock private FeatureFlags mFeatureFlags; + @Mock private KeyguardSecurityModel mKeyguardSecurityModel; + @Mock private PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor; + @Mock private PrimaryBouncerInteractor mPrimaryBouncerInteractor; + @Mock private BouncerView mBouncerView; + @Mock private BouncerViewDelegate mBouncerViewDelegate; + + private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; + private KeyguardBouncer.PrimaryBouncerExpansionCallback mBouncerExpansionCallback; + private FakeKeyguardStateController mKeyguardStateController = + spy(new FakeKeyguardStateController()); + + @Mock private ViewRootImpl mViewRootImpl; + @Mock private WindowOnBackInvokedDispatcher mOnBackInvokedDispatcher; + @Captor + private ArgumentCaptor<OnBackInvokedCallback> mOnBackInvokedCallback; + + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mKeyguardBouncerFactory.create( + any(ViewGroup.class), + any(KeyguardBouncer.PrimaryBouncerExpansionCallback.class))) + .thenReturn(mPrimaryBouncer); + when(mCentralSurfaces.getBouncerContainer()).thenReturn(mContainer); + when(mContainer.findViewById(anyInt())).thenReturn(mKeyguardMessageArea); + when(mKeyguardMessageAreaFactory.create(any(KeyguardMessageArea.class))) + .thenReturn(mKeyguardMessageAreaController); + when(mBouncerView.getDelegate()).thenReturn(mBouncerViewDelegate); + + mStatusBarKeyguardViewManager = + new StatusBarKeyguardViewManager( + getContext(), + mViewMediatorCallback, + mLockPatternUtils, + mStatusBarStateController, + mock(ConfigurationController.class), + mKeyguardUpdateMonitor, + mDreamOverlayStateController, + mock(NavigationModeController.class), + mock(DockManager.class), + mock(NotificationShadeWindowController.class), + mKeyguardStateController, + mock(NotificationMediaManager.class), + mKeyguardBouncerFactory, + mKeyguardMessageAreaFactory, + Optional.of(mSysUiUnfoldComponent), + () -> mShadeController, + mLatencyTracker, + mKeyguardSecurityModel, + mFeatureFlags, + mPrimaryBouncerCallbackInteractor, + mPrimaryBouncerInteractor, + mBouncerView) { + @Override + public ViewRootImpl getViewRootImpl() { + return mViewRootImpl; + } + }; + when(mViewRootImpl.getOnBackInvokedDispatcher()) + .thenReturn(mOnBackInvokedDispatcher); + mStatusBarKeyguardViewManager.registerCentralSurfaces( + mCentralSurfaces, + mNotificationPanelView, + new ShadeExpansionStateManager(), + mBiometricUnlockController, + mNotificationContainer, + mBypassController); + mStatusBarKeyguardViewManager.show(null); + ArgumentCaptor<KeyguardBouncer.PrimaryBouncerExpansionCallback> callbackArgumentCaptor = + ArgumentCaptor.forClass(KeyguardBouncer.PrimaryBouncerExpansionCallback.class); + verify(mKeyguardBouncerFactory).create(any(ViewGroup.class), + callbackArgumentCaptor.capture()); + mBouncerExpansionCallback = callbackArgumentCaptor.getValue(); + } + + @Test + public void dismissWithAction_AfterKeyguardGoneSetToFalse() { + OnDismissAction action = () -> false; + Runnable cancelAction = () -> {}; + mStatusBarKeyguardViewManager.dismissWithAction( + action, cancelAction, false /* afterKeyguardGone */); + verify(mPrimaryBouncer).showWithDismissAction(eq(action), eq(cancelAction)); + } + + @Test + public void showBouncer_onlyWhenShowing() { + mStatusBarKeyguardViewManager.hide(0 /* startTime */, 0 /* fadeoutDuration */); + mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */); + verify(mPrimaryBouncer, never()).show(anyBoolean(), anyBoolean()); + verify(mPrimaryBouncer, never()).show(anyBoolean()); + } + + @Test + public void showBouncer_notWhenBouncerAlreadyShowing() { + mStatusBarKeyguardViewManager.hide(0 /* startTime */, 0 /* fadeoutDuration */); + when(mPrimaryBouncer.isSecure()).thenReturn(true); + mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */); + verify(mPrimaryBouncer, never()).show(anyBoolean(), anyBoolean()); + verify(mPrimaryBouncer, never()).show(anyBoolean()); + } + + @Test + public void showBouncer_showsTheBouncer() { + mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */); + verify(mPrimaryBouncer).show(anyBoolean(), eq(true)); + } + + @Test + public void onPanelExpansionChanged_neverShowsDuringHintAnimation() { + when(mNotificationPanelView.isUnlockHintRunning()).thenReturn(true); + mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT); + verify(mPrimaryBouncer, never()).setExpansion(anyFloat()); + } + + @Test + public void onPanelExpansionChanged_propagatesToBouncerOnlyIfShowing() { + mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT); + verify(mPrimaryBouncer, never()).setExpansion(eq(0.5f)); + + when(mPrimaryBouncer.isShowing()).thenReturn(true); + mStatusBarKeyguardViewManager.onPanelExpansionChanged( + expansionEvent(/* fraction= */ 0.6f, /* expanded= */ false, /* tracking= */ true)); + verify(mPrimaryBouncer).setExpansion(eq(0.6f)); + } + + @Test + public void onPanelExpansionChanged_duplicateEventsAreIgnored() { + when(mPrimaryBouncer.isShowing()).thenReturn(true); + mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT); + verify(mPrimaryBouncer).setExpansion(eq(0.5f)); + + reset(mPrimaryBouncer); + mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT); + verify(mPrimaryBouncer, never()).setExpansion(eq(0.5f)); + } + + @Test + public void onPanelExpansionChanged_hideBouncer_afterKeyguardHidden() { + mStatusBarKeyguardViewManager.hide(0, 0); + when(mPrimaryBouncer.inTransit()).thenReturn(true); + + mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT); + verify(mPrimaryBouncer).setExpansion(eq(KeyguardBouncer.EXPANSION_HIDDEN)); + } + + @Test + public void onPanelExpansionChanged_showsBouncerWhenSwiping() { + mKeyguardStateController.setCanDismissLockScreen(false); + mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT); + verify(mPrimaryBouncer).show(eq(false), eq(false)); + + // But not when it's already visible + reset(mPrimaryBouncer); + when(mPrimaryBouncer.isShowing()).thenReturn(true); + mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT); + verify(mPrimaryBouncer, never()).show(eq(false), eq(false)); + + // Or animating away + reset(mPrimaryBouncer); + when(mPrimaryBouncer.isAnimatingAway()).thenReturn(true); + mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT); + verify(mPrimaryBouncer, never()).show(eq(false), eq(false)); + } + + @Test + public void onPanelExpansionChanged_neverTranslatesBouncerWhenWakeAndUnlock() { + when(mBiometricUnlockController.getMode()) + .thenReturn(BiometricUnlockController.MODE_WAKE_AND_UNLOCK); + mStatusBarKeyguardViewManager.onPanelExpansionChanged( + expansionEvent( + /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE, + /* expanded= */ true, + /* tracking= */ false)); + verify(mPrimaryBouncer, never()).setExpansion(anyFloat()); + } + + @Test + public void onPanelExpansionChanged_neverTranslatesBouncerWhenDismissBouncer() { + // Since KeyguardBouncer.EXPANSION_VISIBLE = 0 panel expansion, if the unlock is dismissing + // the bouncer, there may be an onPanelExpansionChanged(0) call to collapse the panel + // which would mistakenly cause the bouncer to show briefly before its visibility + // is set to hide. Therefore, we don't want to propagate panelExpansionChanged to the + // bouncer if the bouncer is dismissing as a result of a biometric unlock. + when(mBiometricUnlockController.getMode()) + .thenReturn(BiometricUnlockController.MODE_DISMISS_BOUNCER); + mStatusBarKeyguardViewManager.onPanelExpansionChanged( + expansionEvent( + /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE, + /* expanded= */ true, + /* tracking= */ false)); + verify(mPrimaryBouncer, never()).setExpansion(anyFloat()); + } + + @Test + public void onPanelExpansionChanged_neverTranslatesBouncerWhenOccluded() { + when(mKeyguardStateController.isOccluded()).thenReturn(true); + mStatusBarKeyguardViewManager.onPanelExpansionChanged( + expansionEvent( + /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE, + /* expanded= */ true, + /* tracking= */ false)); + verify(mPrimaryBouncer, never()).setExpansion(anyFloat()); + } + + @Test + public void onPanelExpansionChanged_neverTranslatesBouncerWhenShowBouncer() { + // Since KeyguardBouncer.EXPANSION_VISIBLE = 0 panel expansion, if the unlock is dismissing + // the bouncer, there may be an onPanelExpansionChanged(0) call to collapse the panel + // which would mistakenly cause the bouncer to show briefly before its visibility + // is set to hide. Therefore, we don't want to propagate panelExpansionChanged to the + // bouncer if the bouncer is dismissing as a result of a biometric unlock. + when(mBiometricUnlockController.getMode()) + .thenReturn(BiometricUnlockController.MODE_SHOW_BOUNCER); + mStatusBarKeyguardViewManager.onPanelExpansionChanged( + expansionEvent( + /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE, + /* expanded= */ true, + /* tracking= */ false)); + verify(mPrimaryBouncer, never()).setExpansion(anyFloat()); + } + + @Test + public void onPanelExpansionChanged_neverTranslatesBouncerWhenShadeLocked() { + when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE_LOCKED); + mStatusBarKeyguardViewManager.onPanelExpansionChanged( + expansionEvent( + /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE, + /* expanded= */ true, + /* tracking= */ false)); + verify(mPrimaryBouncer, never()).setExpansion(anyFloat()); + } + + @Test + public void setOccluded_animatesPanelExpansion_onlyIfBouncerHidden() { + mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, true /* animated */); + verify(mCentralSurfaces).animateKeyguardUnoccluding(); + + when(mPrimaryBouncer.isShowing()).thenReturn(true); + clearInvocations(mCentralSurfaces); + mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, true /* animated */); + verify(mCentralSurfaces, never()).animateKeyguardUnoccluding(); + } + + @Test + public void setOccluded_onKeyguardOccludedChangedCalled() { + clearInvocations(mKeyguardStateController); + clearInvocations(mKeyguardUpdateMonitor); + + mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, false /* animated */); + verify(mKeyguardStateController).notifyKeyguardState(true, false); + + clearInvocations(mKeyguardUpdateMonitor); + clearInvocations(mKeyguardStateController); + + mStatusBarKeyguardViewManager.setOccluded(true /* occluded */, false /* animated */); + verify(mKeyguardStateController).notifyKeyguardState(true, true); + + clearInvocations(mKeyguardUpdateMonitor); + clearInvocations(mKeyguardStateController); + + mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, false /* animated */); + verify(mKeyguardStateController).notifyKeyguardState(true, false); + } + + @Test + public void setOccluded_isInLaunchTransition_onKeyguardOccludedChangedCalled() { + mStatusBarKeyguardViewManager.show(null); + + mStatusBarKeyguardViewManager.setOccluded(true /* occluded */, false /* animated */); + verify(mKeyguardStateController).notifyKeyguardState(true, true); + } + + @Test + public void setOccluded_isLaunchingActivityOverLockscreen_onKeyguardOccludedChangedCalled() { + when(mCentralSurfaces.isLaunchingActivityOverLockscreen()).thenReturn(true); + mStatusBarKeyguardViewManager.show(null); + + mStatusBarKeyguardViewManager.setOccluded(true /* occluded */, false /* animated */); + verify(mKeyguardStateController).notifyKeyguardState(true, true); + } + + @Test + public void testHiding_cancelsGoneRunnable() { + OnDismissAction action = mock(OnDismissAction.class); + Runnable cancelAction = mock(Runnable.class); + mStatusBarKeyguardViewManager.dismissWithAction( + action, cancelAction, true /* afterKeyguardGone */); + + when(mPrimaryBouncer.isShowing()).thenReturn(false); + mStatusBarKeyguardViewManager.hideBouncer(true); + mStatusBarKeyguardViewManager.hide(0, 30); + verify(action, never()).onDismiss(); + verify(cancelAction).run(); + } + + @Test + public void testHidingBouncer_cancelsGoneRunnable() { + OnDismissAction action = mock(OnDismissAction.class); + Runnable cancelAction = mock(Runnable.class); + mStatusBarKeyguardViewManager.dismissWithAction( + action, cancelAction, true /* afterKeyguardGone */); + + when(mPrimaryBouncer.isShowing()).thenReturn(false); + mStatusBarKeyguardViewManager.hideBouncer(true); + + verify(action, never()).onDismiss(); + verify(cancelAction).run(); + } + + @Test + public void testHiding_doesntCancelWhenShowing() { + OnDismissAction action = mock(OnDismissAction.class); + Runnable cancelAction = mock(Runnable.class); + mStatusBarKeyguardViewManager.dismissWithAction( + action, cancelAction, true /* afterKeyguardGone */); + + mStatusBarKeyguardViewManager.hide(0, 30); + verify(action).onDismiss(); + verify(cancelAction, never()).run(); + } + + @Test + public void testShowing_whenAlternateAuthShowing() { + mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); + when(mPrimaryBouncer.isShowing()).thenReturn(false); + when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true); + assertTrue( + "Is showing not accurate when alternative auth showing", + mStatusBarKeyguardViewManager.isBouncerShowing()); + } + + @Test + public void testWillBeShowing_whenAlternateAuthShowing() { + mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); + when(mPrimaryBouncer.isShowing()).thenReturn(false); + when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true); + assertTrue( + "Is or will be showing not accurate when alternative auth showing", + mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing()); + } + + @Test + public void testHideAlternateBouncer_onShowBouncer() { + // GIVEN alt auth is showing + mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); + when(mPrimaryBouncer.isShowing()).thenReturn(false); + when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true); + reset(mAlternateBouncer); + + // WHEN showBouncer is called + mStatusBarKeyguardViewManager.showPrimaryBouncer(true); + + // THEN alt bouncer should be hidden + verify(mAlternateBouncer).hideAlternateBouncer(); + } + + @Test + public void testBouncerIsOrWillBeShowing_whenBouncerIsInTransit() { + when(mPrimaryBouncer.isShowing()).thenReturn(false); + when(mPrimaryBouncer.inTransit()).thenReturn(true); + + assertTrue( + "Is or will be showing should be true when bouncer is in transit", + mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing()); + } + + @Test + public void testShowAltAuth_unlockingWithBiometricNotAllowed() { + // GIVEN alt auth exists, unlocking with biometric isn't allowed + mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); + when(mPrimaryBouncer.isShowing()).thenReturn(false); + when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) + .thenReturn(false); + + // WHEN showGenericBouncer is called + final boolean scrimmed = true; + mStatusBarKeyguardViewManager.showBouncer(scrimmed); + + // THEN regular bouncer is shown + verify(mPrimaryBouncer).show(anyBoolean(), eq(scrimmed)); + verify(mAlternateBouncer, never()).showAlternateBouncer(); + } + + @Test + public void testShowAlternateBouncer_unlockingWithBiometricAllowed() { + // GIVEN alt auth exists, unlocking with biometric is allowed + mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); + when(mPrimaryBouncer.isShowing()).thenReturn(false); + when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); + + // WHEN showGenericBouncer is called + mStatusBarKeyguardViewManager.showBouncer(true); + + // THEN alt auth bouncer is shown + verify(mAlternateBouncer).showAlternateBouncer(); + verify(mPrimaryBouncer, never()).show(anyBoolean(), anyBoolean()); + } + + @Test + public void testUpdateResources_delegatesToBouncer() { + mStatusBarKeyguardViewManager.updateResources(); + + verify(mPrimaryBouncer).updateResources(); + } + + @Test + public void updateKeyguardPosition_delegatesToBouncer() { + mStatusBarKeyguardViewManager.updateKeyguardPosition(1.0f); + + verify(mPrimaryBouncer).updateKeyguardPosition(1.0f); + } + + @Test + public void testIsBouncerInTransit() { + when(mPrimaryBouncer.inTransit()).thenReturn(true); + Truth.assertThat(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).isTrue(); + when(mPrimaryBouncer.inTransit()).thenReturn(false); + Truth.assertThat(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).isFalse(); + mPrimaryBouncer = null; + Truth.assertThat(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).isFalse(); + } + + private static ShadeExpansionChangeEvent expansionEvent( + float fraction, boolean expanded, boolean tracking) { + return new ShadeExpansionChangeEvent( + fraction, expanded, tracking, /* dragDownPxAmount= */ 0f); + } + + @Test + public void testPredictiveBackCallback_registration() { + /* verify that a predictive back callback is registered when the bouncer becomes visible */ + mBouncerExpansionCallback.onVisibilityChanged(true); + verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback( + eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY), + mOnBackInvokedCallback.capture()); + + /* verify that the same callback is unregistered when the bouncer becomes invisible */ + mBouncerExpansionCallback.onVisibilityChanged(false); + verify(mOnBackInvokedDispatcher).unregisterOnBackInvokedCallback( + eq(mOnBackInvokedCallback.getValue())); + } + + @Test + public void testPredictiveBackCallback_invocationHidesBouncer() { + mBouncerExpansionCallback.onVisibilityChanged(true); + /* capture the predictive back callback during registration */ + verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback( + eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY), + mOnBackInvokedCallback.capture()); + + when(mPrimaryBouncer.isShowing()).thenReturn(true); + when(mCentralSurfaces.shouldKeyguardHideImmediately()).thenReturn(true); + /* invoke the back callback directly */ + mOnBackInvokedCallback.getValue().onBackInvoked(); + + /* verify that the bouncer will be hidden as a result of the invocation */ + verify(mCentralSurfaces).setBouncerShowing(eq(false)); + } + + @Test + public void testReportBouncerOnDreamWhenVisible() { + mBouncerExpansionCallback.onVisibilityChanged(true); + verify(mCentralSurfaces).setBouncerShowingOverDream(false); + Mockito.clearInvocations(mCentralSurfaces); + when(mDreamOverlayStateController.isOverlayActive()).thenReturn(true); + mBouncerExpansionCallback.onVisibilityChanged(true); + verify(mCentralSurfaces).setBouncerShowingOverDream(true); + } + + @Test + public void testReportBouncerOnDreamWhenNotVisible() { + mBouncerExpansionCallback.onVisibilityChanged(false); + verify(mCentralSurfaces).setBouncerShowingOverDream(false); + Mockito.clearInvocations(mCentralSurfaces); + when(mDreamOverlayStateController.isOverlayActive()).thenReturn(true); + mBouncerExpansionCallback.onVisibilityChanged(false); + verify(mCentralSurfaces).setBouncerShowingOverDream(false); + } + + @Test + public void flag_off_DoesNotCallBouncerInteractor() { + when(mFeatureFlags.isEnabled(MODERN_BOUNCER)).thenReturn(false); + mStatusBarKeyguardViewManager.hideBouncer(false); + verify(mPrimaryBouncerInteractor, never()).hide(); + } + + @Test + public void hideAlternateBouncer_beforeCentralSurfacesRegistered() { + mStatusBarKeyguardViewManager = + new StatusBarKeyguardViewManager( + getContext(), + mViewMediatorCallback, + mLockPatternUtils, + mStatusBarStateController, + mock(ConfigurationController.class), + mKeyguardUpdateMonitor, + mDreamOverlayStateController, + mock(NavigationModeController.class), + mock(DockManager.class), + mock(NotificationShadeWindowController.class), + mKeyguardStateController, + mock(NotificationMediaManager.class), + mKeyguardBouncerFactory, + mKeyguardMessageAreaFactory, + Optional.of(mSysUiUnfoldComponent), + () -> mShadeController, + mLatencyTracker, + mKeyguardSecurityModel, + mFeatureFlags, + mPrimaryBouncerCallbackInteractor, + mPrimaryBouncerInteractor, + mBouncerView) { + @Override + public ViewRootImpl getViewRootImpl() { + return mViewRootImpl; + } + }; + + // the following call before registering centralSurfaces should NOT throw a NPE: + mStatusBarKeyguardViewManager.hideAlternateBouncer(true); + } +} 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 new file mode 100644 index 000000000000..96a280a296ed --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt @@ -0,0 +1,219 @@ +/* + * 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.repository + +import android.net.ConnectivityManager +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager +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.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoModeMobileConnectionDataSource +import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.validMobileEvent +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryImpl +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.util.mockito.any +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.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +/** + * The switcher acts as a dispatcher to either the `prod` or `demo` versions of the repository + * interface it's switching on. These tests just need to verify that the entire interface properly + * switches over when the value of `demoMode` changes + */ +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class MobileRepositorySwitcherTest : SysuiTestCase() { + private lateinit var underTest: MobileRepositorySwitcher + private lateinit var realRepo: MobileConnectionsRepositoryImpl + private lateinit var demoRepo: DemoMobileConnectionsRepository + private lateinit var mockDataSource: DemoModeMobileConnectionDataSource + + @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 + + private val globalSettings = FakeSettings() + private val fakeNetworkEventsFlow = MutableStateFlow<FakeNetworkEventModel?>(null) + + private val scope = CoroutineScope(IMMEDIATE) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + // Never start in demo mode + whenever(demoModeController.isInDemoMode).thenReturn(false) + + mockDataSource = + mock<DemoModeMobileConnectionDataSource>().also { + whenever(it.mobileEvents).thenReturn(fakeNetworkEventsFlow) + } + + realRepo = + MobileConnectionsRepositoryImpl( + connectivityManager, + subscriptionManager, + telephonyManager, + logger, + fakeBroadcastDispatcher, + globalSettings, + context, + IMMEDIATE, + scope, + mock(), + ) + + demoRepo = + DemoMobileConnectionsRepository( + dataSource = mockDataSource, + scope = scope, + context = context, + ) + + underTest = + MobileRepositorySwitcher( + scope = scope, + realRepository = realRepo, + demoMobileConnectionsRepository = demoRepo, + demoModeController = demoModeController, + ) + } + + @After + fun tearDown() { + scope.cancel() + } + + @Test + fun `active repo matches demo mode setting`() = + runBlocking(IMMEDIATE) { + whenever(demoModeController.isInDemoMode).thenReturn(false) + + var latest: MobileConnectionsRepository? = null + val job = underTest.activeRepo.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(realRepo) + + startDemoMode() + + assertThat(latest).isEqualTo(demoRepo) + + finishDemoMode() + + assertThat(latest).isEqualTo(realRepo) + + job.cancel() + } + + @Test + fun `subscription list updates when demo mode changes`() = + runBlocking(IMMEDIATE) { + whenever(demoModeController.isInDemoMode).thenReturn(false) + + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_2)) + + var latest: List<SubscriptionInfo>? = null + val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this) + + // The real subscriptions has 2 subs + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_2)) + getSubscriptionCallback().onSubscriptionsChanged() + + assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2)) + + // Demo mode turns on, and we should see only the demo subscriptions + startDemoMode() + fakeNetworkEventsFlow.value = validMobileEvent(subId = 3) + + // Demo mobile connections repository makes arbitrarily-formed subscription info + // objects, so just validate the data we care about + assertThat(latest).hasSize(1) + assertThat(latest!![0].subscriptionId).isEqualTo(3) + + finishDemoMode() + + assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2)) + + job.cancel() + } + + private fun startDemoMode() { + whenever(demoModeController.isInDemoMode).thenReturn(true) + getDemoModeCallback().onDemoModeStarted() + } + + private fun finishDemoMode() { + whenever(demoModeController.isInDemoMode).thenReturn(false) + getDemoModeCallback().onDemoModeFinished() + } + + private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener { + val callbackCaptor = + kotlinArgumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>() + verify(subscriptionManager) + .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture()) + return callbackCaptor.value + } + + private fun getDemoModeCallback(): DemoMode { + val captor = kotlinArgumentCaptor<DemoMode>() + verify(demoModeController).addCallback(captor.capture()) + return captor.value + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + + private const val SUB_1_ID = 1 + private val SUB_1 = + mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) } + + private const val SUB_2_ID = 2 + private val SUB_2 = + mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) } + } +} 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 new file mode 100644 index 000000000000..bf5ecd895c40 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt @@ -0,0 +1,249 @@ +/* + * 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.repository.demo + +import android.telephony.Annotation +import android.telephony.TelephonyManager +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.statusbar.pipeline.mobile.data.model.DataConnectionState +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters + +/** + * Parameterized test for all of the common values of [FakeNetworkEventModel]. This test simply + * verifies that passing the given model to [DemoMobileConnectionsRepository] results in the correct + * flows emitting from the given connection. + */ +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(Parameterized::class) +internal class DemoMobileConnectionParameterizedTest(private val testCase: TestCase) : + SysuiTestCase() { + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + private val fakeNetworkEventFlow = MutableStateFlow<FakeNetworkEventModel?>(null) + + private lateinit var connectionsRepo: DemoMobileConnectionsRepository + private lateinit var underTest: DemoMobileConnectionRepository + private lateinit var mockDataSource: DemoModeMobileConnectionDataSource + + @Before + fun setUp() { + // The data source only provides one API, so we can mock it with a flow here for convenience + mockDataSource = + mock<DemoModeMobileConnectionDataSource>().also { + whenever(it.mobileEvents).thenReturn(fakeNetworkEventFlow) + } + + connectionsRepo = + DemoMobileConnectionsRepository( + dataSource = mockDataSource, + scope = testScope.backgroundScope, + context = context, + ) + + connectionsRepo.startProcessingCommands() + } + + @After + fun tearDown() { + testScope.cancel() + } + + @Test + fun demoNetworkData() = + testScope.runTest { + val networkModel = + FakeNetworkEventModel.Mobile( + level = testCase.level, + dataType = testCase.dataType, + subId = testCase.subId, + carrierId = testCase.carrierId, + inflateStrength = testCase.inflateStrength, + activity = testCase.activity, + carrierNetworkChange = testCase.carrierNetworkChange, + ) + + fakeNetworkEventFlow.value = networkModel + underTest = connectionsRepo.getRepoForSubId(subId) + + assertConnection(underTest, networkModel) + } + + private fun assertConnection( + conn: DemoMobileConnectionRepository, + model: FakeNetworkEventModel + ) { + when (model) { + is FakeNetworkEventModel.Mobile -> { + val subscriptionModel: MobileSubscriptionModel = conn.subscriptionModelFlow.value + assertThat(conn.subId).isEqualTo(model.subId) + assertThat(subscriptionModel.cdmaLevel).isEqualTo(model.level) + assertThat(subscriptionModel.primaryLevel).isEqualTo(model.level) + assertThat(subscriptionModel.dataActivityDirection).isEqualTo(model.activity) + assertThat(subscriptionModel.carrierNetworkChangeActive) + .isEqualTo(model.carrierNetworkChange) + + // TODO(b/261029387): check these once we start handling them + assertThat(subscriptionModel.isEmergencyOnly).isFalse() + assertThat(subscriptionModel.isGsm).isFalse() + assertThat(subscriptionModel.dataConnectionState) + .isEqualTo(DataConnectionState.Connected) + } + // MobileDisabled isn't combinatorial in nature, and is tested in + // DemoMobileConnectionsRepositoryTest.kt + else -> {} + } + } + + /** Matches [FakeNetworkEventModel] */ + internal data class TestCase( + val level: Int, + val dataType: SignalIcon.MobileIconGroup, + val subId: Int, + val carrierId: Int, + val inflateStrength: Boolean, + @Annotation.DataActivityType val activity: Int, + val carrierNetworkChange: Boolean, + ) { + override fun toString(): String { + return "INPUT(level=$level, " + + "dataType=${dataType.name}, " + + "subId=$subId, " + + "carrierId=$carrierId, " + + "inflateStrength=$inflateStrength, " + + "activity=$activity, " + + "carrierNetworkChange=$carrierNetworkChange)" + } + + // Convenience for iterating test data and creating new cases + fun modifiedBy( + level: Int? = null, + dataType: SignalIcon.MobileIconGroup? = null, + subId: Int? = null, + carrierId: Int? = null, + inflateStrength: Boolean? = null, + @Annotation.DataActivityType activity: Int? = null, + carrierNetworkChange: Boolean? = null, + ): TestCase = + TestCase( + level = level ?: this.level, + dataType = dataType ?: this.dataType, + subId = subId ?: this.subId, + carrierId = carrierId ?: this.carrierId, + inflateStrength = inflateStrength ?: this.inflateStrength, + activity = activity ?: this.activity, + carrierNetworkChange = carrierNetworkChange ?: this.carrierNetworkChange + ) + } + + companion object { + private val subId = 1 + + private val booleanList = listOf(true, false) + private val levels = listOf(0, 1, 2, 3) + private val dataTypes = + listOf( + TelephonyIcons.THREE_G, + TelephonyIcons.LTE, + TelephonyIcons.FOUR_G, + TelephonyIcons.NR_5G, + TelephonyIcons.NR_5G_PLUS, + ) + private val carrierIds = listOf(1, 10, 100) + private val inflateStrength = booleanList + private val activity = + listOf( + TelephonyManager.DATA_ACTIVITY_NONE, + TelephonyManager.DATA_ACTIVITY_IN, + TelephonyManager.DATA_ACTIVITY_OUT, + TelephonyManager.DATA_ACTIVITY_INOUT + ) + private val carrierNetworkChange = booleanList + + @Parameters(name = "{0}") @JvmStatic fun data() = testData() + + /** + * Generate some test data. For the sake of convenience, we'll parameterize only non-null + * network event data. So given the lists of test data: + * ``` + * list1 = [1, 2, 3] + * list2 = [false, true] + * list3 = [a, b, c] + * ``` + * We'll generate test cases for: + * + * Test (1, false, a) Test (2, false, a) Test (3, false, a) Test (1, true, a) Test (1, + * false, b) Test (1, false, c) + * + * NOTE: this is not a combinatorial product of all of the possible sets of parameters. + * Since this test is built to exercise demo mode, the general approach is to define a + * fully-formed "base case", and from there to make sure to use every valid parameter once, + * by defining the rest of the test cases against the base case. Specific use-cases can be + * added to the non-parameterized test, or manually below the generated test cases. + */ + private fun testData(): List<TestCase> { + val testSet = mutableSetOf<TestCase>() + + val baseCase = + TestCase( + levels.first(), + dataTypes.first(), + subId, + carrierIds.first(), + inflateStrength.first(), + activity.first(), + carrierNetworkChange.first() + ) + + val tail = + sequenceOf( + levels.map { baseCase.modifiedBy(level = it) }, + dataTypes.map { baseCase.modifiedBy(dataType = it) }, + carrierIds.map { baseCase.modifiedBy(carrierId = it) }, + inflateStrength.map { baseCase.modifiedBy(inflateStrength = it) }, + activity.map { baseCase.modifiedBy(activity = it) }, + carrierNetworkChange.map { baseCase.modifiedBy(carrierNetworkChange = it) }, + ) + .flatten() + + testSet.add(baseCase) + tail.toCollection(testSet) + + return testSet.toList() + } + } +} 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 new file mode 100644 index 000000000000..a8f6993373d2 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt @@ -0,0 +1,306 @@ +/* + * 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.repository.demo + +import android.telephony.SubscriptionInfo +import android.telephony.TelephonyManager.DATA_ACTIVITY_INOUT +import android.telephony.TelephonyManager.UNKNOWN_CARRIER_ID +import androidx.test.filters.SmallTest +import com.android.settingslib.SignalIcon +import com.android.settingslib.mobile.TelephonyIcons.THREE_G +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import junit.framework.Assert +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 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + private val fakeNetworkEventFlow = MutableStateFlow<FakeNetworkEventModel?>(null) + + private lateinit var underTest: DemoMobileConnectionsRepository + private lateinit var mockDataSource: DemoModeMobileConnectionDataSource + + @Before + fun setUp() { + // The data source only provides one API, so we can mock it with a flow here for convenience + mockDataSource = + mock<DemoModeMobileConnectionDataSource>().also { + whenever(it.mobileEvents).thenReturn(fakeNetworkEventFlow) + } + + underTest = + DemoMobileConnectionsRepository( + dataSource = mockDataSource, + scope = testScope.backgroundScope, + context = context, + ) + + underTest.startProcessingCommands() + } + + @Test + fun `network event - create new subscription`() = + testScope.runTest { + var latest: List<SubscriptionInfo>? = null + val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEmpty() + + fakeNetworkEventFlow.value = validMobileEvent(subId = 1) + + assertThat(latest).hasSize(1) + assertThat(latest!![0].subscriptionId).isEqualTo(1) + + job.cancel() + } + + @Test + fun `network event - reuses subscription when same Id`() = + testScope.runTest { + var latest: List<SubscriptionInfo>? = null + val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEmpty() + + fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1) + + assertThat(latest).hasSize(1) + assertThat(latest!![0].subscriptionId).isEqualTo(1) + + // Second network event comes in with the same subId, does not create a new subscription + fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 2) + + assertThat(latest).hasSize(1) + assertThat(latest!![0].subscriptionId).isEqualTo(1) + + job.cancel() + } + + @Test + fun `multiple subscriptions`() = + testScope.runTest { + var latest: List<SubscriptionInfo>? = null + val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this) + + fakeNetworkEventFlow.value = validMobileEvent(subId = 1) + fakeNetworkEventFlow.value = validMobileEvent(subId = 2) + + assertThat(latest).hasSize(2) + + job.cancel() + } + + @Test + fun `mobile disabled event - disables connection - subId specified - single conn`() = + testScope.runTest { + var latest: List<SubscriptionInfo>? = null + val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this) + + fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1) + + fakeNetworkEventFlow.value = MobileDisabled(subId = 1) + + assertThat(latest).hasSize(0) + + job.cancel() + } + + @Test + fun `mobile disabled event - disables connection - subId not specified - single conn`() = + testScope.runTest { + var latest: List<SubscriptionInfo>? = null + val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this) + + fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1) + + fakeNetworkEventFlow.value = MobileDisabled(subId = null) + + assertThat(latest).hasSize(0) + + job.cancel() + } + + @Test + fun `mobile disabled event - disables connection - subId specified - multiple conn`() = + testScope.runTest { + var latest: List<SubscriptionInfo>? = null + val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this) + + fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1) + fakeNetworkEventFlow.value = validMobileEvent(subId = 2, level = 1) + + fakeNetworkEventFlow.value = MobileDisabled(subId = 2) + + assertThat(latest).hasSize(1) + + job.cancel() + } + + @Test + fun `mobile disabled event - subId not specified - multiple conn - ignores command`() = + testScope.runTest { + var latest: List<SubscriptionInfo>? = null + val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this) + + fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1) + fakeNetworkEventFlow.value = validMobileEvent(subId = 2, level = 1) + + fakeNetworkEventFlow.value = MobileDisabled(subId = null) + + assertThat(latest).hasSize(2) + + job.cancel() + } + + @Test + fun `demo connection - single subscription`() = + testScope.runTest { + var currentEvent: FakeNetworkEventModel = validMobileEvent(subId = 1) + var connections: List<DemoMobileConnectionRepository>? = null + val job = + underTest.subscriptionsFlow + .onEach { infos -> + connections = + infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) } + } + .launchIn(this) + + fakeNetworkEventFlow.value = currentEvent + + assertThat(connections).hasSize(1) + val connection1 = connections!![0] + + assertConnection(connection1, currentEvent) + + // Exercise the whole api + + currentEvent = validMobileEvent(subId = 1, level = 2) + fakeNetworkEventFlow.value = currentEvent + assertConnection(connection1, currentEvent) + + job.cancel() + } + + @Test + fun `demo connection - two connections - update second - no affect on first`() = + testScope.runTest { + var currentEvent1 = validMobileEvent(subId = 1) + var connection1: DemoMobileConnectionRepository? = null + var currentEvent2 = validMobileEvent(subId = 2) + var connection2: DemoMobileConnectionRepository? = null + var connections: List<DemoMobileConnectionRepository>? = null + val job = + underTest.subscriptionsFlow + .onEach { infos -> + connections = + infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) } + } + .launchIn(this) + + fakeNetworkEventFlow.value = currentEvent1 + fakeNetworkEventFlow.value = currentEvent2 + assertThat(connections).hasSize(2) + connections!!.forEach { + if (it.subId == 1) { + connection1 = it + } else if (it.subId == 2) { + connection2 = it + } else { + Assert.fail("Unexpected subscription") + } + } + + assertConnection(connection1!!, currentEvent1) + assertConnection(connection2!!, currentEvent2) + + // WHEN the event changes for connection 2, it updates, and connection 1 stays the same + currentEvent2 = validMobileEvent(subId = 2, activity = DATA_ACTIVITY_INOUT) + fakeNetworkEventFlow.value = currentEvent2 + assertConnection(connection1!!, currentEvent1) + assertConnection(connection2!!, currentEvent2) + + // and vice versa + currentEvent1 = validMobileEvent(subId = 1, inflateStrength = true) + fakeNetworkEventFlow.value = currentEvent1 + assertConnection(connection1!!, currentEvent1) + assertConnection(connection2!!, currentEvent2) + + job.cancel() + } + + private fun assertConnection( + conn: DemoMobileConnectionRepository, + model: FakeNetworkEventModel + ) { + when (model) { + is FakeNetworkEventModel.Mobile -> { + val subscriptionModel: MobileSubscriptionModel = conn.subscriptionModelFlow.value + assertThat(conn.subId).isEqualTo(model.subId) + assertThat(subscriptionModel.cdmaLevel).isEqualTo(model.level) + assertThat(subscriptionModel.primaryLevel).isEqualTo(model.level) + assertThat(subscriptionModel.dataActivityDirection).isEqualTo(model.activity) + assertThat(subscriptionModel.carrierNetworkChangeActive) + .isEqualTo(model.carrierNetworkChange) + + // TODO(b/261029387) check these once we start handling them + assertThat(subscriptionModel.isEmergencyOnly).isFalse() + assertThat(subscriptionModel.isGsm).isFalse() + assertThat(subscriptionModel.dataConnectionState) + .isEqualTo(DataConnectionState.Connected) + } + else -> {} + } + } +} + +/** Convenience to create a valid fake network event with minimal params */ +fun validMobileEvent( + level: Int? = 1, + dataType: SignalIcon.MobileIconGroup? = THREE_G, + subId: Int? = 1, + carrierId: Int? = UNKNOWN_CARRIER_ID, + inflateStrength: Boolean? = false, + activity: Int? = null, + carrierNetworkChange: Boolean = false, +): FakeNetworkEventModel = + FakeNetworkEventModel.Mobile( + level = level, + dataType = dataType, + subId = subId, + carrierId = carrierId, + inflateStrength = inflateStrength, + activity = activity, + carrierNetworkChange = carrierNetworkChange, + ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt index 5ce51bb62c78..c8df5ac17dff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.statusbar.pipeline.mobile.data.repository +package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod import android.os.UserHandle import android.provider.Settings @@ -31,6 +31,7 @@ import android.telephony.TelephonyManager.DATA_CONNECTED import android.telephony.TelephonyManager.DATA_CONNECTING import android.telephony.TelephonyManager.DATA_DISCONNECTED import android.telephony.TelephonyManager.DATA_DISCONNECTING +import android.telephony.TelephonyManager.DATA_UNKNOWN import android.telephony.TelephonyManager.NETWORK_TYPE_LTE import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN import androidx.test.filters.SmallTest @@ -39,6 +40,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionS import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor @@ -221,6 +223,21 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { } @Test + fun testFlowForSubId_dataConnectionState_unknown() = + runBlocking(IMMEDIATE) { + var latest: MobileSubscriptionModel? = null + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) + + val callback = + getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() + callback.onDataConnectionStateChanged(DATA_UNKNOWN, 200 /* unused */) + + assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Unknown) + + job.cancel() + } + + @Test fun testFlowForSubId_dataActivity() = runBlocking(IMMEDIATE) { var latest: MobileSubscriptionModel? = null diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt index a953a3d802e6..359ea18fcb84 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.statusbar.pipeline.mobile.data.repository +package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod import android.content.Intent import android.net.ConnectivityManager diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/TestableAlertDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/TestableAlertDialogTest.kt new file mode 100644 index 000000000000..01dd60ae2200 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/util/TestableAlertDialogTest.kt @@ -0,0 +1,333 @@ +/* + * 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.util + +import android.content.DialogInterface +import android.content.DialogInterface.BUTTON_NEGATIVE +import android.content.DialogInterface.BUTTON_NEUTRAL +import android.content.DialogInterface.BUTTON_POSITIVE +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.inOrder +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class TestableAlertDialogTest : SysuiTestCase() { + + @Test + fun dialogNotShowingWhenCreated() { + val dialog = TestableAlertDialog(context) + + assertThat(dialog.isShowing).isFalse() + } + + @Test + fun dialogShownDoesntCrash() { + val dialog = TestableAlertDialog(context) + + dialog.show() + } + + @Test + fun dialogShowing() { + val dialog = TestableAlertDialog(context) + + dialog.show() + + assertThat(dialog.isShowing).isTrue() + } + + @Test + fun showListenerCalled() { + val dialog = TestableAlertDialog(context) + val listener: DialogInterface.OnShowListener = mock() + dialog.setOnShowListener(listener) + + dialog.show() + + verify(listener).onShow(dialog) + } + + @Test + fun showListenerRemoved() { + val dialog = TestableAlertDialog(context) + val listener: DialogInterface.OnShowListener = mock() + dialog.setOnShowListener(listener) + dialog.setOnShowListener(null) + + dialog.show() + + verify(listener, never()).onShow(any()) + } + + @Test + fun dialogHiddenNotShowing() { + val dialog = TestableAlertDialog(context) + + dialog.show() + dialog.hide() + + assertThat(dialog.isShowing).isFalse() + } + + @Test + fun dialogDismissNotShowing() { + val dialog = TestableAlertDialog(context) + + dialog.show() + dialog.dismiss() + + assertThat(dialog.isShowing).isFalse() + } + + @Test + fun dismissListenerCalled_ifShowing() { + val dialog = TestableAlertDialog(context) + val listener: DialogInterface.OnDismissListener = mock() + dialog.setOnDismissListener(listener) + + dialog.show() + dialog.dismiss() + + verify(listener).onDismiss(dialog) + } + + @Test + fun dismissListenerNotCalled_ifNotShowing() { + val dialog = TestableAlertDialog(context) + val listener: DialogInterface.OnDismissListener = mock() + dialog.setOnDismissListener(listener) + + dialog.dismiss() + + verify(listener, never()).onDismiss(any()) + } + + @Test + fun dismissListenerRemoved() { + val dialog = TestableAlertDialog(context) + val listener: DialogInterface.OnDismissListener = mock() + dialog.setOnDismissListener(listener) + dialog.setOnDismissListener(null) + + dialog.show() + dialog.dismiss() + + verify(listener, never()).onDismiss(any()) + } + + @Test + fun cancelListenerCalled_showing() { + val dialog = TestableAlertDialog(context) + val listener: DialogInterface.OnCancelListener = mock() + dialog.setOnCancelListener(listener) + + dialog.show() + dialog.cancel() + + verify(listener).onCancel(dialog) + } + + @Test + fun cancelListenerCalled_notShowing() { + val dialog = TestableAlertDialog(context) + val listener: DialogInterface.OnCancelListener = mock() + dialog.setOnCancelListener(listener) + + dialog.cancel() + + verify(listener).onCancel(dialog) + } + + @Test + fun dismissCalledOnCancel_showing() { + val dialog = TestableAlertDialog(context) + val listener: DialogInterface.OnDismissListener = mock() + dialog.setOnDismissListener(listener) + + dialog.show() + dialog.cancel() + + verify(listener).onDismiss(dialog) + } + + @Test + fun dialogCancelNotShowing() { + val dialog = TestableAlertDialog(context) + + dialog.show() + dialog.cancel() + + assertThat(dialog.isShowing).isFalse() + } + + @Test + fun cancelListenerRemoved() { + val dialog = TestableAlertDialog(context) + val listener: DialogInterface.OnCancelListener = mock() + dialog.setOnCancelListener(listener) + dialog.setOnCancelListener(null) + + dialog.show() + dialog.cancel() + + verify(listener, never()).onCancel(any()) + } + + @Test + fun positiveButtonClick() { + val dialog = TestableAlertDialog(context) + val listener: DialogInterface.OnClickListener = mock() + dialog.setButton(BUTTON_POSITIVE, "", listener) + + dialog.show() + dialog.clickButton(BUTTON_POSITIVE) + + verify(listener).onClick(dialog, BUTTON_POSITIVE) + } + + @Test + fun positiveButtonListener_noCalledWhenClickOtherButtons() { + val dialog = TestableAlertDialog(context) + val listener: DialogInterface.OnClickListener = mock() + dialog.setButton(BUTTON_POSITIVE, "", listener) + + dialog.show() + dialog.clickButton(BUTTON_NEUTRAL) + dialog.clickButton(BUTTON_NEGATIVE) + + verify(listener, never()).onClick(any(), anyInt()) + } + + @Test + fun negativeButtonClick() { + val dialog = TestableAlertDialog(context) + val listener: DialogInterface.OnClickListener = mock() + dialog.setButton(BUTTON_NEGATIVE, "", listener) + + dialog.show() + dialog.clickButton(BUTTON_NEGATIVE) + + verify(listener).onClick(dialog, DialogInterface.BUTTON_NEGATIVE) + } + + @Test + fun negativeButtonListener_noCalledWhenClickOtherButtons() { + val dialog = TestableAlertDialog(context) + val listener: DialogInterface.OnClickListener = mock() + dialog.setButton(BUTTON_NEGATIVE, "", listener) + + dialog.show() + dialog.clickButton(BUTTON_NEUTRAL) + dialog.clickButton(BUTTON_POSITIVE) + + verify(listener, never()).onClick(any(), anyInt()) + } + + @Test + fun neutralButtonClick() { + val dialog = TestableAlertDialog(context) + val listener: DialogInterface.OnClickListener = mock() + dialog.setButton(BUTTON_NEUTRAL, "", listener) + + dialog.show() + dialog.clickButton(BUTTON_NEUTRAL) + + verify(listener).onClick(dialog, BUTTON_NEUTRAL) + } + + @Test + fun neutralButtonListener_noCalledWhenClickOtherButtons() { + val dialog = TestableAlertDialog(context) + val listener: DialogInterface.OnClickListener = mock() + dialog.setButton(BUTTON_NEUTRAL, "", listener) + + dialog.show() + dialog.clickButton(BUTTON_POSITIVE) + dialog.clickButton(BUTTON_NEGATIVE) + + verify(listener, never()).onClick(any(), anyInt()) + } + + @Test + fun sameClickListenerCalledCorrectly() { + val dialog = TestableAlertDialog(context) + val listener: DialogInterface.OnClickListener = mock() + dialog.setButton(BUTTON_POSITIVE, "", listener) + dialog.setButton(BUTTON_NEUTRAL, "", listener) + dialog.setButton(BUTTON_NEGATIVE, "", listener) + + dialog.show() + dialog.clickButton(BUTTON_POSITIVE) + dialog.clickButton(BUTTON_NEGATIVE) + dialog.clickButton(BUTTON_NEUTRAL) + + val inOrder = inOrder(listener) + inOrder.verify(listener).onClick(dialog, BUTTON_POSITIVE) + inOrder.verify(listener).onClick(dialog, BUTTON_NEGATIVE) + inOrder.verify(listener).onClick(dialog, BUTTON_NEUTRAL) + } + + @Test(expected = IllegalArgumentException::class) + fun clickBadButton() { + val dialog = TestableAlertDialog(context) + + dialog.clickButton(10000) + } + + @Test + fun clickButtonDismisses_positive() { + val dialog = TestableAlertDialog(context) + + dialog.show() + dialog.clickButton(BUTTON_POSITIVE) + + assertThat(dialog.isShowing).isFalse() + } + + @Test + fun clickButtonDismisses_negative() { + val dialog = TestableAlertDialog(context) + + dialog.show() + dialog.clickButton(BUTTON_NEGATIVE) + + assertThat(dialog.isShowing).isFalse() + } + + @Test + fun clickButtonDismisses_neutral() { + val dialog = TestableAlertDialog(context) + + dialog.show() + dialog.clickButton(BUTTON_NEUTRAL) + + assertThat(dialog.isShowing).isFalse() + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/TestableAlertDialog.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/TestableAlertDialog.kt new file mode 100644 index 000000000000..4d79554a79ce --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/TestableAlertDialog.kt @@ -0,0 +1,141 @@ +/* + * 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.util + +import android.app.AlertDialog +import android.content.Context +import android.content.DialogInterface +import java.lang.IllegalArgumentException + +/** + * [AlertDialog] that is easier to test. Due to [AlertDialog] being a class and not an interface, + * there are some things that cannot be avoided, like the creation of a [Handler] on the main thread + * (and therefore needing a prepared [Looper] in the test). + * + * It bypasses calls to show, clicks on buttons, cancel and dismiss so it all can happen bounded in + * the test. It tries to be as close in behavior as a real [AlertDialog]. + * + * It will only call [onCreate] as part of its lifecycle, but not any of the other lifecycle methods + * in [Dialog]. + * + * In order to test clicking on buttons, use [clickButton] instead of calling [View.callOnClick] on + * the view returned by [getButton] to bypass the internal [Handler]. + */ +class TestableAlertDialog(context: Context) : AlertDialog(context) { + + private var _onDismissListener: DialogInterface.OnDismissListener? = null + private var _onCancelListener: DialogInterface.OnCancelListener? = null + private var _positiveButtonClickListener: DialogInterface.OnClickListener? = null + private var _negativeButtonClickListener: DialogInterface.OnClickListener? = null + private var _neutralButtonClickListener: DialogInterface.OnClickListener? = null + private var _onShowListener: DialogInterface.OnShowListener? = null + private var _dismissOverride: Runnable? = null + + private var showing = false + private var visible = false + private var created = false + + override fun show() { + if (!created) { + created = true + onCreate(null) + } + if (isShowing) return + showing = true + visible = true + _onShowListener?.onShow(this) + } + + override fun hide() { + visible = false + } + + override fun isShowing(): Boolean { + return visible && showing + } + + override fun dismiss() { + if (!showing) { + return + } + if (_dismissOverride != null) { + _dismissOverride?.run() + return + } + _onDismissListener?.onDismiss(this) + showing = false + } + + override fun cancel() { + _onCancelListener?.onCancel(this) + dismiss() + } + + override fun setOnDismissListener(listener: DialogInterface.OnDismissListener?) { + _onDismissListener = listener + } + + override fun setOnCancelListener(listener: DialogInterface.OnCancelListener?) { + _onCancelListener = listener + } + + override fun setOnShowListener(listener: DialogInterface.OnShowListener?) { + _onShowListener = listener + } + + override fun takeCancelAndDismissListeners( + msg: String?, + cancel: DialogInterface.OnCancelListener?, + dismiss: DialogInterface.OnDismissListener? + ): Boolean { + _onCancelListener = cancel + _onDismissListener = dismiss + return true + } + + override fun setButton( + whichButton: Int, + text: CharSequence?, + listener: DialogInterface.OnClickListener? + ) { + super.setButton(whichButton, text, listener) + when (whichButton) { + DialogInterface.BUTTON_POSITIVE -> _positiveButtonClickListener = listener + DialogInterface.BUTTON_NEGATIVE -> _negativeButtonClickListener = listener + DialogInterface.BUTTON_NEUTRAL -> _neutralButtonClickListener = listener + else -> Unit + } + } + + /** + * Click one of the buttons in the [AlertDialog] and call the corresponding listener. + * + * Button ids are from [DialogInterface]. + */ + fun clickButton(whichButton: Int) { + val listener = + when (whichButton) { + DialogInterface.BUTTON_POSITIVE -> _positiveButtonClickListener + DialogInterface.BUTTON_NEGATIVE -> _negativeButtonClickListener + DialogInterface.BUTTON_NEUTRAL -> _neutralButtonClickListener + else -> throw IllegalArgumentException("Wrong button $whichButton") + } + listener?.onClick(this, whichButton) + dismiss() + } +} diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index bc083f1ca01e..19b5cc93932b 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -369,12 +369,10 @@ import com.android.internal.util.FastPrintWriter; import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.MemInfoReader; import com.android.internal.util.Preconditions; -import com.android.internal.util.function.DecFunction; import com.android.internal.util.function.HeptFunction; import com.android.internal.util.function.HexFunction; import com.android.internal.util.function.QuadFunction; import com.android.internal.util.function.QuintFunction; -import com.android.internal.util.function.TriFunction; import com.android.internal.util.function.UndecFunction; import com.android.server.AlarmManagerInternal; import com.android.server.DeviceIdleInternal; @@ -18329,19 +18327,20 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override - public SyncNotedAppOp startProxyOperation(int code, + public SyncNotedAppOp startProxyOperation(@NonNull IBinder clientId, int code, @NonNull AttributionSource attributionSource, boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage, boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags, @AttributionFlags int proxiedAttributionFlags, int attributionChainId, - @NonNull DecFunction<Integer, AttributionSource, Boolean, Boolean, String, Boolean, - Boolean, Integer, Integer, Integer, SyncNotedAppOp> superImpl) { + @NonNull UndecFunction<IBinder, Integer, AttributionSource, + Boolean, Boolean, String, Boolean, Boolean, Integer, Integer, Integer, + SyncNotedAppOp> superImpl) { if (attributionSource.getUid() == mTargetUid && isTargetOp(code)) { final int shellUid = UserHandle.getUid(UserHandle.getUserId( attributionSource.getUid()), Process.SHELL_UID); final long identity = Binder.clearCallingIdentity(); try { - return superImpl.apply(code, new AttributionSource(shellUid, + return superImpl.apply(clientId, code, new AttributionSource(shellUid, "com.android.shell", attributionSource.getAttributionTag(), attributionSource.getToken(), attributionSource.getNext()), startIfModeDefault, shouldCollectAsyncNotedOp, message, @@ -18351,21 +18350,22 @@ public class ActivityManagerService extends IActivityManager.Stub Binder.restoreCallingIdentity(identity); } } - return superImpl.apply(code, attributionSource, startIfModeDefault, + return superImpl.apply(clientId, code, attributionSource, startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage, skipProxyOperation, proxyAttributionFlags, proxiedAttributionFlags, attributionChainId); } @Override - public void finishProxyOperation(int code, @NonNull AttributionSource attributionSource, - boolean skipProxyOperation, @NonNull TriFunction<Integer, AttributionSource, - Boolean, Void> superImpl) { + public void finishProxyOperation(@NonNull IBinder clientId, int code, + @NonNull AttributionSource attributionSource, boolean skipProxyOperation, + @NonNull QuadFunction<IBinder, Integer, AttributionSource, Boolean, + Void> superImpl) { if (attributionSource.getUid() == mTargetUid && isTargetOp(code)) { final int shellUid = UserHandle.getUid(UserHandle.getUserId( attributionSource.getUid()), Process.SHELL_UID); final long identity = Binder.clearCallingIdentity(); try { - superImpl.apply(code, new AttributionSource(shellUid, + superImpl.apply(clientId, code, new AttributionSource(shellUid, "com.android.shell", attributionSource.getAttributionTag(), attributionSource.getToken(), attributionSource.getNext()), skipProxyOperation); @@ -18373,7 +18373,7 @@ public class ActivityManagerService extends IActivityManager.Stub Binder.restoreCallingIdentity(identity); } } - superImpl.apply(code, attributionSource, skipProxyOperation); + superImpl.apply(clientId, code, attributionSource, skipProxyOperation); } private boolean isTargetOp(int code) { diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index e31c952e10f9..20f0c17bb9d9 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -3865,18 +3865,18 @@ public class AppOpsService extends IAppOpsService.Stub { } @Override - public SyncNotedAppOp startProxyOperation(int code, + public SyncNotedAppOp startProxyOperation(@NonNull IBinder clientId, int code, @NonNull AttributionSource attributionSource, boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage, boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags, @AttributionFlags int proxiedAttributionFlags, int attributionChainId) { - return mCheckOpsDelegateDispatcher.startProxyOperation(code, attributionSource, + return mCheckOpsDelegateDispatcher.startProxyOperation(clientId, code, attributionSource, startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage, skipProxyOperation, proxyAttributionFlags, proxiedAttributionFlags, attributionChainId); } - private SyncNotedAppOp startProxyOperationImpl(int code, + private SyncNotedAppOp startProxyOperationImpl(@NonNull IBinder clientId, int code, @NonNull AttributionSource attributionSource, boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage, boolean skipProxyOperation, @AttributionFlags @@ -3885,11 +3885,9 @@ public class AppOpsService extends IAppOpsService.Stub { final int proxyUid = attributionSource.getUid(); final String proxyPackageName = attributionSource.getPackageName(); final String proxyAttributionTag = attributionSource.getAttributionTag(); - final IBinder proxyToken = attributionSource.getToken(); final int proxiedUid = attributionSource.getNextUid(); final String proxiedPackageName = attributionSource.getNextPackageName(); final String proxiedAttributionTag = attributionSource.getNextAttributionTag(); - final IBinder proxiedToken = attributionSource.getNextToken(); verifyIncomingProxyUid(attributionSource); verifyIncomingOp(code); @@ -3928,7 +3926,7 @@ public class AppOpsService extends IAppOpsService.Stub { if (!skipProxyOperation) { // Test if the proxied operation will succeed before starting the proxy operation - final SyncNotedAppOp testProxiedOp = startOperationUnchecked(proxiedToken, code, + final SyncNotedAppOp testProxiedOp = startOperationUnchecked(clientId, code, proxiedUid, resolvedProxiedPackageName, proxiedAttributionTag, proxyUid, resolvedProxyPackageName, proxyAttributionTag, proxiedFlags, startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage, @@ -3940,7 +3938,7 @@ public class AppOpsService extends IAppOpsService.Stub { final int proxyFlags = isProxyTrusted ? AppOpsManager.OP_FLAG_TRUSTED_PROXY : AppOpsManager.OP_FLAG_UNTRUSTED_PROXY; - final SyncNotedAppOp proxyAppOp = startOperationUnchecked(proxyToken, code, proxyUid, + final SyncNotedAppOp proxyAppOp = startOperationUnchecked(clientId, code, proxyUid, resolvedProxyPackageName, proxyAttributionTag, Process.INVALID_UID, null, null, proxyFlags, startIfModeDefault, !isProxyTrusted, "proxy " + message, shouldCollectMessage, proxyAttributionFlags, attributionChainId, @@ -3950,7 +3948,7 @@ public class AppOpsService extends IAppOpsService.Stub { } } - return startOperationUnchecked(proxiedToken, code, proxiedUid, resolvedProxiedPackageName, + return startOperationUnchecked(clientId, code, proxiedUid, resolvedProxiedPackageName, proxiedAttributionTag, proxyUid, resolvedProxyPackageName, proxyAttributionTag, proxiedFlags, startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage, proxiedAttributionFlags, attributionChainId, @@ -4091,22 +4089,20 @@ public class AppOpsService extends IAppOpsService.Stub { } @Override - public void finishProxyOperation(int code, @NonNull AttributionSource attributionSource, - boolean skipProxyOperation) { - mCheckOpsDelegateDispatcher.finishProxyOperation(code, attributionSource, + public void finishProxyOperation(@NonNull IBinder clientId, int code, + @NonNull AttributionSource attributionSource, boolean skipProxyOperation) { + mCheckOpsDelegateDispatcher.finishProxyOperation(clientId, code, attributionSource, skipProxyOperation); } - private Void finishProxyOperationImpl(int code, @NonNull AttributionSource attributionSource, - boolean skipProxyOperation) { + private Void finishProxyOperationImpl(IBinder clientId, int code, + @NonNull AttributionSource attributionSource, boolean skipProxyOperation) { final int proxyUid = attributionSource.getUid(); final String proxyPackageName = attributionSource.getPackageName(); final String proxyAttributionTag = attributionSource.getAttributionTag(); - final IBinder proxyToken = attributionSource.getToken(); final int proxiedUid = attributionSource.getNextUid(); final String proxiedPackageName = attributionSource.getNextPackageName(); final String proxiedAttributionTag = attributionSource.getNextAttributionTag(); - final IBinder proxiedToken = attributionSource.getNextToken(); skipProxyOperation = skipProxyOperation && isCallerAndAttributionTrusted(attributionSource); @@ -4123,7 +4119,7 @@ public class AppOpsService extends IAppOpsService.Stub { } if (!skipProxyOperation) { - finishOperationUnchecked(proxyToken, code, proxyUid, resolvedProxyPackageName, + finishOperationUnchecked(clientId, code, proxyUid, resolvedProxyPackageName, proxyAttributionTag); } @@ -4133,7 +4129,7 @@ public class AppOpsService extends IAppOpsService.Stub { return null; } - finishOperationUnchecked(proxiedToken, code, proxiedUid, resolvedProxiedPackageName, + finishOperationUnchecked(clientId, code, proxiedUid, resolvedProxiedPackageName, proxiedAttributionTag); return null; @@ -7726,42 +7722,42 @@ public class AppOpsService extends IAppOpsService.Stub { attributionFlags, attributionChainId, AppOpsService.this::startOperationImpl); } - public SyncNotedAppOp startProxyOperation(int code, + public SyncNotedAppOp startProxyOperation(@NonNull IBinder clientId, int code, @NonNull AttributionSource attributionSource, boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage, boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags, @AttributionFlags int proxiedAttributionFlags, int attributionChainId) { if (mPolicy != null) { if (mCheckOpsDelegate != null) { - return mPolicy.startProxyOperation(code, attributionSource, + return mPolicy.startProxyOperation(clientId, code, attributionSource, startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage, skipProxyOperation, proxyAttributionFlags, proxiedAttributionFlags, attributionChainId, this::startDelegateProxyOperationImpl); } else { - return mPolicy.startProxyOperation(code, attributionSource, + return mPolicy.startProxyOperation(clientId, code, attributionSource, startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage, skipProxyOperation, proxyAttributionFlags, proxiedAttributionFlags, attributionChainId, AppOpsService.this::startProxyOperationImpl); } } else if (mCheckOpsDelegate != null) { - return startDelegateProxyOperationImpl(code, attributionSource, + return startDelegateProxyOperationImpl(clientId, code, attributionSource, startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage, skipProxyOperation, proxyAttributionFlags, proxiedAttributionFlags, attributionChainId); } - return startProxyOperationImpl(code, attributionSource, startIfModeDefault, + return startProxyOperationImpl(clientId, code, attributionSource, startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage, skipProxyOperation, proxyAttributionFlags, proxiedAttributionFlags, attributionChainId); } - private SyncNotedAppOp startDelegateProxyOperationImpl(int code, + private SyncNotedAppOp startDelegateProxyOperationImpl(@NonNull IBinder clientId, int code, @NonNull AttributionSource attributionSource, boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage, boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags, @AttributionFlags int proxiedAttributionFlsgs, int attributionChainId) { - return mCheckOpsDelegate.startProxyOperation(code, attributionSource, + return mCheckOpsDelegate.startProxyOperation(clientId, code, attributionSource, startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage, skipProxyOperation, proxyAttributionFlags, proxiedAttributionFlsgs, attributionChainId, AppOpsService.this::startProxyOperationImpl); @@ -7790,27 +7786,28 @@ public class AppOpsService extends IAppOpsService.Stub { AppOpsService.this::finishOperationImpl); } - public void finishProxyOperation(int code, + public void finishProxyOperation(@NonNull IBinder clientId, int code, @NonNull AttributionSource attributionSource, boolean skipProxyOperation) { if (mPolicy != null) { if (mCheckOpsDelegate != null) { - mPolicy.finishProxyOperation(code, attributionSource, + mPolicy.finishProxyOperation(clientId, code, attributionSource, skipProxyOperation, this::finishDelegateProxyOperationImpl); } else { - mPolicy.finishProxyOperation(code, attributionSource, + mPolicy.finishProxyOperation(clientId, code, attributionSource, skipProxyOperation, AppOpsService.this::finishProxyOperationImpl); } } else if (mCheckOpsDelegate != null) { - finishDelegateProxyOperationImpl(code, attributionSource, skipProxyOperation); + finishDelegateProxyOperationImpl(clientId, code, attributionSource, + skipProxyOperation); } else { - finishProxyOperationImpl(code, attributionSource, skipProxyOperation); + finishProxyOperationImpl(clientId, code, attributionSource, skipProxyOperation); } } - private Void finishDelegateProxyOperationImpl(int code, + private Void finishDelegateProxyOperationImpl(@NonNull IBinder clientId, int code, @NonNull AttributionSource attributionSource, boolean skipProxyOperation) { - mCheckOpsDelegate.finishProxyOperation(code, attributionSource, skipProxyOperation, - AppOpsService.this::finishProxyOperationImpl); + mCheckOpsDelegate.finishProxyOperation(clientId, code, attributionSource, + skipProxyOperation, AppOpsService.this::finishProxyOperationImpl); return null; } } diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java index 37538db43ddc..d648d6fc663d 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java @@ -1099,7 +1099,7 @@ public class PermissionManagerService extends IPermissionManager.Stub { if (resolvedPackageName == null) { return; } - appOpsManager.finishOp(accessorSource.getToken(), op, + appOpsManager.finishOp(attributionSourceState.token, op, accessorSource.getUid(), resolvedPackageName, accessorSource.getAttributionTag()); } else { @@ -1108,8 +1108,9 @@ public class PermissionManagerService extends IPermissionManager.Stub { if (resolvedAttributionSource.getPackageName() == null) { return; } - appOpsManager.finishProxyOp(AppOpsManager.opToPublicName(op), - resolvedAttributionSource, skipCurrentFinish); + appOpsManager.finishProxyOp(attributionSourceState.token, + AppOpsManager.opToPublicName(op), resolvedAttributionSource, + skipCurrentFinish); } RegisteredAttribution registered = @@ -1225,10 +1226,11 @@ public class PermissionManagerService extends IPermissionManager.Stub { && next.getNext() == null); final boolean selfAccess = singleReceiverFromDatasource || next == null; - final int opMode = performOpTransaction(context, op, current, message, - forDataDelivery, /*startDataDelivery*/ false, skipCurrentChecks, - selfAccess, singleReceiverFromDatasource, AppOpsManager.OP_NONE, - AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_FLAGS_NONE, + final int opMode = performOpTransaction(context, attributionSource.getToken(), op, + current, message, forDataDelivery, /*startDataDelivery*/ false, + skipCurrentChecks, selfAccess, singleReceiverFromDatasource, + AppOpsManager.OP_NONE, AppOpsManager.ATTRIBUTION_FLAGS_NONE, + AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE); switch (opMode) { @@ -1331,10 +1333,10 @@ public class PermissionManagerService extends IPermissionManager.Stub { attributionSource, next, fromDatasource, startDataDelivery, selfAccess, isLinkTrusted) : ATTRIBUTION_FLAGS_NONE; - final int opMode = performOpTransaction(context, op, current, message, - forDataDelivery, startDataDelivery, skipCurrentChecks, selfAccess, - singleReceiverFromDatasource, attributedOp, proxyAttributionFlags, - proxiedAttributionFlags, attributionChainId); + final int opMode = performOpTransaction(context, attributionSource.getToken(), op, + current, message, forDataDelivery, startDataDelivery, skipCurrentChecks, + selfAccess, singleReceiverFromDatasource, attributedOp, + proxyAttributionFlags, proxiedAttributionFlags, attributionChainId); switch (opMode) { case AppOpsManager.MODE_ERRORED: { @@ -1479,8 +1481,8 @@ public class PermissionManagerService extends IPermissionManager.Stub { attributionSource, next, /*fromDatasource*/ false, startDataDelivery, selfAccess, isLinkTrusted) : ATTRIBUTION_FLAGS_NONE; - final int opMode = performOpTransaction(context, op, current, message, - forDataDelivery, startDataDelivery, skipCurrentChecks, selfAccess, + final int opMode = performOpTransaction(context, current.getToken(), op, current, + message, forDataDelivery, startDataDelivery, skipCurrentChecks, selfAccess, /*fromDatasource*/ false, AppOpsManager.OP_NONE, proxyAttributionFlags, proxiedAttributionFlags, attributionChainId); @@ -1502,7 +1504,8 @@ public class PermissionManagerService extends IPermissionManager.Stub { } @SuppressWarnings("ConstantConditions") - private static int performOpTransaction(@NonNull Context context, int op, + private static int performOpTransaction(@NonNull Context context, + @NonNull IBinder chainStartToken, int op, @NonNull AttributionSource attributionSource, @Nullable String message, boolean forDataDelivery, boolean startDataDelivery, boolean skipProxyOperation, boolean selfAccess, boolean singleReceiverFromDatasource, int attributedOp, @@ -1564,7 +1567,7 @@ public class PermissionManagerService extends IPermissionManager.Stub { if (selfAccess) { try { startedOpResult = appOpsManager.startOpNoThrow( - resolvedAttributionSource.getToken(), startedOp, + chainStartToken, startedOp, resolvedAttributionSource.getUid(), resolvedAttributionSource.getPackageName(), /*startIfModeDefault*/ false, @@ -1575,14 +1578,14 @@ public class PermissionManagerService extends IPermissionManager.Stub { + " platform defined runtime permission " + AppOpsManager.opToPermission(op) + " while not having " + Manifest.permission.UPDATE_APP_OPS_STATS); - startedOpResult = appOpsManager.startProxyOpNoThrow(attributedOp, - attributionSource, message, skipProxyOperation, + startedOpResult = appOpsManager.startProxyOpNoThrow(chainStartToken, + attributedOp, attributionSource, message, skipProxyOperation, proxyAttributionFlags, proxiedAttributionFlags, attributionChainId); } } else { try { - startedOpResult = appOpsManager.startProxyOpNoThrow(startedOp, - resolvedAttributionSource, message, skipProxyOperation, + startedOpResult = appOpsManager.startProxyOpNoThrow(chainStartToken, + startedOp, resolvedAttributionSource, message, skipProxyOperation, proxyAttributionFlags, proxiedAttributionFlags, attributionChainId); } catch (SecurityException e) { //TODO 195339480: remove diff --git a/services/core/java/com/android/server/policy/AppOpsPolicy.java b/services/core/java/com/android/server/policy/AppOpsPolicy.java index ebd9126d1439..b26c1b921b56 100644 --- a/services/core/java/com/android/server/policy/AppOpsPolicy.java +++ b/services/core/java/com/android/server/policy/AppOpsPolicy.java @@ -45,13 +45,11 @@ import android.util.Log; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; -import com.android.internal.util.function.DecFunction; import com.android.internal.util.function.HeptFunction; import com.android.internal.util.function.HexFunction; import com.android.internal.util.function.QuadFunction; import com.android.internal.util.function.QuintConsumer; import com.android.internal.util.function.QuintFunction; -import com.android.internal.util.function.TriFunction; import com.android.internal.util.function.UndecFunction; import com.android.server.LocalServices; @@ -256,14 +254,14 @@ public final class AppOpsPolicy implements AppOpsManagerInternal.CheckOpsDelegat } @Override - public SyncNotedAppOp startProxyOperation(int code, + public SyncNotedAppOp startProxyOperation(@NonNull IBinder clientId, int code, @NonNull AttributionSource attributionSource, boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage, boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags, @AttributionFlags int proxiedAttributionFlags, int attributionChainId, - @NonNull DecFunction<Integer, AttributionSource, Boolean, Boolean, String, Boolean, - Boolean, Integer, Integer, Integer, SyncNotedAppOp> superImpl) { - return superImpl.apply(resolveDatasourceOp(code, attributionSource.getUid(), + @NonNull UndecFunction<IBinder, Integer, AttributionSource, Boolean, Boolean, String, + Boolean, Boolean, Integer, Integer, Integer, SyncNotedAppOp> superImpl) { + return superImpl.apply(clientId, resolveDatasourceOp(code, attributionSource.getUid(), attributionSource.getPackageName(), attributionSource.getAttributionTag()), attributionSource, startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage, skipProxyOperation, proxyAttributionFlags, @@ -279,10 +277,10 @@ public final class AppOpsPolicy implements AppOpsManagerInternal.CheckOpsDelegat } @Override - public void finishProxyOperation(int code, @NonNull AttributionSource attributionSource, - boolean skipProxyOperation, @NonNull TriFunction<Integer, AttributionSource, - Boolean, Void> superImpl) { - superImpl.apply(resolveDatasourceOp(code, attributionSource.getUid(), + public void finishProxyOperation(@NonNull IBinder clientId, int code, + @NonNull AttributionSource attributionSource, boolean skipProxyOperation, + @NonNull QuadFunction<IBinder, Integer, AttributionSource, Boolean, Void> superImpl) { + superImpl.apply(clientId, resolveDatasourceOp(code, attributionSource.getUid(), attributionSource.getPackageName(), attributionSource.getAttributionTag()), attributionSource, skipProxyOperation); } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index a4c9684af418..6d5da3254eac 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -2873,6 +2873,18 @@ public class PhoneWindowManager implements WindowManagerPolicy { return key_consumed; } break; + case KeyEvent.KEYCODE_DPAD_LEFT: + if (down && event.isMetaPressed() && event.isCtrlPressed() && repeatCount == 0) { + enterStageSplitFromRunningApp(true /* leftOrTop */); + return key_consumed; + } + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (down && event.isMetaPressed() && event.isCtrlPressed() && repeatCount == 0) { + enterStageSplitFromRunningApp(false /* leftOrTop */); + return key_consumed; + } + break; case KeyEvent.KEYCODE_SLASH: if (down && repeatCount == 0 && event.isMetaPressed() && !keyguardOn) { toggleKeyboardShortcutsMenu(event.getDeviceId()); @@ -3489,6 +3501,13 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } + private void enterStageSplitFromRunningApp(boolean leftOrTop) { + StatusBarManagerInternal statusbar = getStatusBarManagerInternal(); + if (statusbar != null) { + statusbar.enterStageSplitFromRunningApp(leftOrTop); + } + } + void launchHomeFromHotKey(int displayId) { launchHomeFromHotKey(displayId, true /* awakenFromDreams */, true /*respectKeyguard*/); } diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java index 9957140162a0..e7221c850da8 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java @@ -184,4 +184,11 @@ public interface StatusBarManagerInternal { * Called when requested to go to fullscreen from the active split app. */ void goToFullscreenFromSplit(); + + /** + * Enters stage split from a current running app. + * + * @see com.android.internal.statusbar.IStatusBar#enterStageSplitFromRunningApp + */ + void enterStageSplitFromRunningApp(boolean leftOrTop); } diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 5a91dc6ef8d9..45748e6ce76d 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -705,6 +705,15 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } catch (RemoteException ex) { } } } + + @Override + public void enterStageSplitFromRunningApp(boolean leftOrTop) { + if (mBar != null) { + try { + mBar.enterStageSplitFromRunningApp(leftOrTop); + } catch (RemoteException ex) { } + } + } }; private final GlobalActionsProvider mGlobalActionsProvider = new GlobalActionsProvider() { diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 76a112283220..51eec03855a5 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -2183,7 +2183,7 @@ class Task extends TaskFragment { } private boolean shouldStartChangeTransition(int prevWinMode, @NonNull Rect prevBounds) { - if (!isLeafTask() || !canStartChangeTransition()) { + if (!(isLeafTask() || mCreatedByOrganizer) || !canStartChangeTransition()) { return false; } final int newWinMode = getWindowingMode(); diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index d3d1c163aa19..971b619e671f 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -38,6 +38,7 @@ import android.os.SystemProperties; import android.util.ArrayMap; import android.util.Slog; import android.util.proto.ProtoOutputStream; +import android.view.SurfaceControl; import android.view.WindowManager; import android.window.ITransitionMetricsReporter; import android.window.ITransitionPlayer; @@ -116,6 +117,8 @@ class TransitionController { */ boolean mBuildingFinishLayers = false; + private final SurfaceControl.Transaction mWakeT = new SurfaceControl.Transaction(); + TransitionController(ActivityTaskManagerService atm, TaskSnapshotController taskSnapshotController, TransitionTracer transitionTracer) { @@ -619,8 +622,16 @@ class TransitionController { private void updateRunningRemoteAnimation(Transition transition, boolean isPlaying) { if (mTransitionPlayerProc == null) return; if (isPlaying) { + mWakeT.setEarlyWakeupStart(); + mWakeT.apply(); + // Usually transitions put quite a load onto the system already (with all the things + // happening in app), so pause task snapshot persisting to not increase the load. + mAtm.mWindowManager.mTaskSnapshotController.setPersisterPaused(true); mTransitionPlayerProc.setRunningRemoteAnimation(true); } else if (mPlayingTransitions.isEmpty()) { + mWakeT.setEarlyWakeupEnd(); + mWakeT.apply(); + mAtm.mWindowManager.mTaskSnapshotController.setPersisterPaused(false); mTransitionPlayerProc.setRunningRemoteAnimation(false); mRemotePlayer.clear(); return; |