Merge "AudioService: do not log sound effects in audio dumpsys" into tm-qpr-dev
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index cb64173..9344d96 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -9134,8 +9134,9 @@
*/
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 @@
*
* @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 @@
}
}
- 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 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 @@
*
* @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 4d6e4ae..43023fe 100644
--- a/core/java/android/app/AppOpsManagerInternal.java
+++ b/core/java/android/app/AppOpsManagerInternal.java
@@ -26,13 +26,11 @@
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 @@
/**
* 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 @@
* @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 @@
*
* @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/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java
index 719b5b6..13fc2e6 100644
--- a/core/java/android/app/StatusBarManager.java
+++ b/core/java/android/app/StatusBarManager.java
@@ -234,7 +234,10 @@
/**
* Session flag for {@link #registerSessionListener} indicating the listener
- * is interested in sessions on the keygaurd
+ * is interested in sessions on the keygaurd.
+ * Keyguard Session Boundaries:
+ * START_SESSION: device starts going to sleep OR the keyguard is newly shown
+ * END_SESSION: device starts going to sleep OR keyguard is no longer showing
* @hide
*/
public static final int SESSION_KEYGUARD = 1 << 0;
diff --git a/core/java/android/service/controls/ControlsProviderService.java b/core/java/android/service/controls/ControlsProviderService.java
index d2a4ae2..9396a88 100644
--- a/core/java/android/service/controls/ControlsProviderService.java
+++ b/core/java/android/service/controls/ControlsProviderService.java
@@ -69,6 +69,18 @@
"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 30da4b4..88447da 100644
--- a/core/java/com/android/internal/app/IAppOpsService.aidl
+++ b/core/java/com/android/internal/app/IAppOpsService.aidl
@@ -58,11 +58,12 @@
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 2cf41bb..017bf3f 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -331,4 +331,11 @@
/** 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 5763345..9410e06 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 694040a..9722e79 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 6e116b9..c836b95 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.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 @@
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 @@
mIsResizing = true;
mBounds.set(newBounds);
}
- mResizingBounds.set(newBounds);
mOffsetX = offsetX;
mOffsetY = offsetY;
@@ -227,33 +225,41 @@
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 @@
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 @@
* 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 @@
mFadeAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(@NonNull Animator animation) {
+ mRunningAnimationCount++;
if (show) {
animT.show(mBackgroundLeash).show(mIconLeash);
}
@@ -335,6 +363,7 @@
@Override
public void onAnimationEnd(@NonNull Animator animation) {
+ mRunningAnimationCount--;
if (!show) {
if (mBackgroundLeash != null) {
animT.hide(mBackgroundLeash);
@@ -343,11 +372,15 @@
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 400039b..9329d02 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 @@
@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 @@
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 @@
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 @@
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 21a1310..1cf3a89 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 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 @@
DismissTransition mPendingDismiss = null;
TransitSession mPendingEnter = null;
TransitSession mPendingRecent = null;
+ TransitSession mPendingResize = null;
private IBinder mAnimatingTransition = null;
OneShotRemoteHandler mPendingRemoteHandler = null;
@@ -177,6 +179,43 @@
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 @@
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 @@
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 @@
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 @@
mPendingRecent.onConsumed(aborted);
mPendingRecent = null;
mPendingRemoteHandler = null;
+ } else if (isPendingResize(transition)) {
+ mPendingResize.onConsumed(aborted);
+ mPendingResize = null;
}
}
@@ -340,6 +404,9 @@
} 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 2dc4a04..1016e1b 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__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_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 @@
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 aa0512b..da8dc87 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 @@
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 @@
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 @@
}
}
- mSyncQueue.queue(evictWct);
+
+ if (!isEnteringSplit && openingToSide) {
+ final WindowContainerTransaction evictWct = new WindowContainerTransaction();
+ prepareEvictNonOpeningChildTasks(position, apps, evictWct);
+ mSyncQueue.queue(evictWct);
+ }
}
};
@@ -1667,15 +1669,29 @@
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 @@
} 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 358f712..8a52c87 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 @@
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 @@
}
}
+ 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 17ad55f..8acc2f8 100644
--- a/packages/SystemUI/animation/Android.bp
+++ b/packages/SystemUI/animation/Android.bp
@@ -44,23 +44,3 @@
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 3dc8510..0000000
--- a/packages/SystemUI/animation/TEST_MAPPING
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "presubmit": [
- {
- "name": "SystemUIAnimationLibTests"
- }
- ]
-}
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_num_pad_key.xml b/packages/SystemUI/res-keyguard/layout/keyguard_num_pad_key.xml
index 316ad39..411fea5 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_num_pad_key.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_num_pad_key.xml
@@ -18,8 +18,6 @@
<TextView
android:id="@+id/digit_text"
style="@style/Widget.TextView.NumPadKey.Digit"
- android:autoSizeMaxTextSize="32sp"
- android:autoSizeTextType="uniform"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt
index d172690..d85292a 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt
@@ -38,6 +38,7 @@
const val ACTION_SET_FLAG = "com.android.systemui.action.SET_FLAG"
const val ACTION_GET_FLAGS = "com.android.systemui.action.GET_FLAGS"
const val FLAGS_PERMISSION = "com.android.systemui.permission.FLAGS"
+ const val ACTION_SYSUI_STARTED = "com.android.systemui.STARTED"
const val EXTRA_ID = "id"
const val EXTRA_VALUE = "value"
const val EXTRA_FLAGS = "flags"
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
index 02a6d7b..e6f559b 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -210,8 +210,10 @@
(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 @@
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 @@
}
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 @@
mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
updateConfiguration();
+ Trace.endSection();
}
@VisibleForTesting
@@ -521,6 +527,12 @@
}
private void setupDecorations() {
+ Trace.beginSection("ScreenDecorations#setupDecorations");
+ setupDecorationsInner();
+ Trace.endSection();
+ }
+
+ private void setupDecorationsInner() {
if (hasRoundedCorners() || shouldDrawCutout() || isPrivacyDotEnabled()
|| mFaceScanningFactory.getHasProviders()) {
@@ -573,7 +585,11 @@
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 @@
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 @@
}
mExecutor.execute(() -> {
+ Trace.beginSection("ScreenDecorations#onConfigurationChanged");
int oldRotation = mRotation;
mPendingConfigChange = false;
updateConfiguration();
@@ -951,6 +972,7 @@
// the updated rotation).
updateLayoutParams();
}
+ Trace.endSection();
});
}
@@ -1119,6 +1141,7 @@
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 @@
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 e09e65e..17ebdad 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 @@
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 fb37def..63c2065 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 @@
} 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 4c8e1ac..a07c716 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.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.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 @@
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 @@
} else {
items[0]
}
-
maybeUpdateSelectedItem(selectionItem)
createControlsSpaceFrame()
@@ -374,11 +376,20 @@
}
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 @@
println("hidden: $hidden")
println("selectedItem: $selectedItem")
println("lastSelections: $lastSelections")
+ println("setting: ${controlsSettingsRepository
+ .allowActionOnTrivialControlsInLockscreen.value}")
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java
index 5dae0a2..d1a14a1 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java
@@ -86,7 +86,7 @@
new ServerFlagReader.ChangeListener() {
@Override
public void onChange() {
- mRestarter.restart();
+ mRestarter.restartSystemUI();
}
};
@@ -326,9 +326,7 @@
Log.i(TAG, "SystemUI Restart Suppressed");
return;
}
- Log.i(TAG, "Restarting SystemUI");
- // SysUI starts back when up exited. Is there a better way to do this?
- System.exit(0);
+ mRestarter.restartSystemUI();
}
private void restartAndroid(boolean requestSuppress) {
@@ -336,7 +334,7 @@
Log.i(TAG, "Android Restart Suppressed");
return;
}
- mRestarter.restart();
+ mRestarter.restartAndroid();
}
void setBooleanFlagInternal(Flag<?> flag, boolean value) {
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt
index 3d9f627..069e612 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt
@@ -28,6 +28,8 @@
private val systemExitRestarter: SystemExitRestarter,
) : Restarter {
+ private var androidRestartRequested = false
+
val observer =
object : WakefulnessLifecycle.Observer {
override fun onFinishedGoingToSleep() {
@@ -36,8 +38,18 @@
}
}
- override fun restart() {
- Log.d(FeatureFlagsDebug.TAG, "Restart requested. Restarting on next screen off.")
+ override fun restartSystemUI() {
+ Log.d(FeatureFlagsDebug.TAG, "SystemUI Restart requested. Restarting on next screen off.")
+ scheduleRestart()
+ }
+
+ override fun restartAndroid() {
+ Log.d(FeatureFlagsDebug.TAG, "Android Restart requested. Restarting on next screen off.")
+ androidRestartRequested = true
+ scheduleRestart()
+ }
+
+ fun scheduleRestart() {
if (wakefulnessLifecycle.wakefulness == WakefulnessLifecycle.WAKEFULNESS_ASLEEP) {
restartNow()
} else {
@@ -46,6 +58,10 @@
}
private fun restartNow() {
- systemExitRestarter.restart()
+ if (androidRestartRequested) {
+ systemExitRestarter.restartAndroid()
+ } else {
+ systemExitRestarter.restartSystemUI()
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt
index 7189f00..b94d781 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt
@@ -16,7 +16,9 @@
package com.android.systemui.flags
+import android.content.Intent
import com.android.systemui.CoreStartable
+import com.android.systemui.broadcast.BroadcastSender
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.commandline.CommandRegistry
import dagger.Binds
@@ -31,7 +33,8 @@
dumpManager: DumpManager,
private val commandRegistry: CommandRegistry,
private val flagCommand: FlagCommand,
- private val featureFlags: FeatureFlagsDebug
+ private val featureFlags: FeatureFlagsDebug,
+ private val broadcastSender: BroadcastSender
) : CoreStartable {
init {
@@ -43,6 +46,8 @@
override fun start() {
featureFlags.init()
commandRegistry.registerCommand(FlagCommand.FLAG_COMMAND) { flagCommand }
+ val intent = Intent(FlagManager.ACTION_SYSUI_STARTED)
+ broadcastSender.sendBroadcast(intent)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java
index 3c83682..8bddacc 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java
@@ -61,7 +61,7 @@
new ServerFlagReader.ChangeListener() {
@Override
public void onChange() {
- mRestarter.restart();
+ mRestarter.restartSystemUI();
}
};
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt
index a3f0f66..7ff3876 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt
@@ -34,35 +34,48 @@
@Background private val bgExecutor: DelayableExecutor,
private val systemExitRestarter: SystemExitRestarter
) : Restarter {
- var shouldRestart = false
+ var listenersAdded = false
var pendingRestart: Runnable? = null
+ var androidRestartRequested = false
val observer =
object : WakefulnessLifecycle.Observer {
override fun onFinishedGoingToSleep() {
- maybeScheduleRestart()
+ scheduleRestart()
}
}
val batteryCallback =
object : BatteryController.BatteryStateChangeCallback {
override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) {
- maybeScheduleRestart()
+ scheduleRestart()
}
}
- override fun restart() {
- Log.d(FeatureFlagsDebug.TAG, "Restart requested. Restarting when plugged in and idle.")
- if (!shouldRestart) {
- // Don't bother scheduling twice.
- shouldRestart = true
- wakefulnessLifecycle.addObserver(observer)
- batteryController.addCallback(batteryCallback)
- maybeScheduleRestart()
- }
+ override fun restartSystemUI() {
+ Log.d(
+ FeatureFlagsDebug.TAG,
+ "SystemUI Restart requested. Restarting when plugged in and idle."
+ )
+ scheduleRestart()
}
- private fun maybeScheduleRestart() {
+ override fun restartAndroid() {
+ Log.d(
+ FeatureFlagsDebug.TAG,
+ "Android Restart requested. Restarting when plugged in and idle."
+ )
+ androidRestartRequested = true
+ scheduleRestart()
+ }
+
+ private fun scheduleRestart() {
+ // Don't bother adding listeners twice.
+ if (!listenersAdded) {
+ listenersAdded = true
+ wakefulnessLifecycle.addObserver(observer)
+ batteryController.addCallback(batteryCallback)
+ }
if (
wakefulnessLifecycle.wakefulness == WAKEFULNESS_ASLEEP && batteryController.isPluggedIn
) {
@@ -77,6 +90,10 @@
private fun restartNow() {
Log.d(FeatureFlagsRelease.TAG, "Restarting due to systemui flag change")
- systemExitRestarter.restart()
+ if (androidRestartRequested) {
+ systemExitRestarter.restartAndroid()
+ } else {
+ systemExitRestarter.restartSystemUI()
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Restarter.kt b/packages/SystemUI/src/com/android/systemui/flags/Restarter.kt
index 8f095a2..ce8b821 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Restarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Restarter.kt
@@ -16,5 +16,7 @@
package com.android.systemui.flags
interface Restarter {
- fun restart()
-}
\ No newline at end of file
+ fun restartSystemUI()
+
+ fun restartAndroid()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/SystemExitRestarter.kt b/packages/SystemUI/src/com/android/systemui/flags/SystemExitRestarter.kt
index f1b1be4..89daa64 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/SystemExitRestarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/SystemExitRestarter.kt
@@ -16,10 +16,19 @@
package com.android.systemui.flags
+import com.android.internal.statusbar.IStatusBarService
import javax.inject.Inject
-class SystemExitRestarter @Inject constructor() : Restarter {
- override fun restart() {
+class SystemExitRestarter
+@Inject
+constructor(
+ private val barService: IStatusBarService,
+) : Restarter {
+ override fun restartAndroid() {
+ barService.restart()
+ }
+
+ override fun restartSystemUI() {
System.exit(0)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
index a908e94..0c46b23 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
@@ -829,7 +829,11 @@
surfaceBehindEntryAnimator.cancel()
surfaceBehindAlpha = 1f
setSurfaceBehindAppearAmount(1f)
- launcherUnlockController?.setUnlockAmount(1f, false /* forceIfAnimating */)
+ try {
+ launcherUnlockController?.setUnlockAmount(1f, false /* forceIfAnimating */)
+ } catch (e: RemoteException) {
+ Log.e(TAG, "Remote exception in notifyFinishedKeyguardExitAnimation", e)
+ }
// That target is no longer valid since the animation finished, null it out.
surfaceBehindRemoteAnimationTargets = null
diff --git a/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java b/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java
index c7e4c5e..b98a92f 100644
--- a/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java
+++ b/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java
@@ -49,7 +49,9 @@
@SysUISingleton
public class SessionTracker implements CoreStartable {
private static final String TAG = "SessionTracker";
- private static final boolean DEBUG = false;
+
+ // To enable logs: `adb shell setprop log.tag.SessionTracker DEBUG` & restart sysui
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
// At most 20 bits: ~1m possibilities, ~0.5% probability of collision in 100 values
private final InstanceIdSequence mInstanceIdGenerator = new InstanceIdSequence(1 << 20);
@@ -81,8 +83,8 @@
mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);
mKeyguardStateController.addCallback(mKeyguardStateCallback);
- mKeyguardSessionStarted = mKeyguardStateController.isShowing();
- if (mKeyguardSessionStarted) {
+ if (mKeyguardStateController.isShowing()) {
+ mKeyguardSessionStarted = true;
startSession(SESSION_KEYGUARD);
}
}
@@ -136,12 +138,11 @@
new KeyguardUpdateMonitorCallback() {
@Override
public void onStartedGoingToSleep(int why) {
- // we need to register to the KeyguardUpdateMonitor lifecycle b/c it gets called
- // before the WakefulnessLifecycle
if (mKeyguardSessionStarted) {
- return;
+ endSession(SESSION_KEYGUARD);
}
+ // Start a new session whenever the device goes to sleep
mKeyguardSessionStarted = true;
startSession(SESSION_KEYGUARD);
}
@@ -154,6 +155,9 @@
boolean wasSessionStarted = mKeyguardSessionStarted;
boolean keyguardShowing = mKeyguardStateController.isShowing();
if (keyguardShowing && !wasSessionStarted) {
+ // the keyguard can start showing without the device going to sleep (ie: lockdown
+ // from the power button), so we start a new keyguard session when the keyguard is
+ // newly shown in addition to when the device starts going to sleep
mKeyguardSessionStarted = true;
startSession(SESSION_KEYGUARD);
} else if (!keyguardShowing && wasSessionStarted) {
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 3012bb4..2dd339d 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 @@
appUid = appUid
)
mediaEntries.put(packageName, resumeData)
+ logSingleVsMultipleMediaAdded(appUid, packageName, instanceId)
logger.logResumeMediaAdded(appUid, packageName, instanceId)
}
backgroundExecutor.execute {
@@ -812,6 +813,7 @@
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 @@
}
}
+ 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 3ad8c21..ea943be 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 @@
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 @@
@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 647beb9..b10abb5 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 @@
/** 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],
+ )
- private fun getUndoCallback(
- @StatusBarManager.MediaTransferSenderState displayState: Int
- ): Runnable? {
- return if (isSucceededState(displayState)) {
- Runnable { Log.i(CLI_TAG, "Undo triggered for $displayState") }
- } else {
- null
+ if (args.size == 2) {
+ return senderArgs
}
+
+ // 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 @@
}
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 3fd1aa7..e2f55f0 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 @@
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 cb0f3e2..d03ac3b 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 @@
}
// 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 6240c10..cad296b 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 @@
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 e43d4c8..51de522 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 @@
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 7130294..a6c7781 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.view.View;
import android.widget.Switch;
+import androidx.annotation.MainThread;
import androidx.annotation.Nullable;
import com.android.internal.logging.MetricsLogger;
@@ -91,11 +92,13 @@
}
@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 547b496..00d129a 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 @@
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 d000e6e..750d004 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 @@
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 @@
* @see IStatusBar#goToFullscreenFromSplit
*/
default void goToFullscreenFromSplit() {}
+
+ /**
+ * @see IStatusBar#enterStageSplitFromRunningApp
+ */
+ default void enterStageSplitFromRunningApp(boolean leftOrTop) {}
}
public CommandQueue(Context context) {
@@ -1248,6 +1254,14 @@
}
@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 @@
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 97a47b5..362764d 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.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 @@
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 @@
TelephonyListenerManager telephonyListenerManager,
@Nullable WifiManager wifiManager,
AccessPointControllerImpl accessPointController,
+ StatusBarPipelineFlags statusBarPipelineFlags,
DemoModeController demoModeController,
CarrierConfigTracker carrierConfigTracker,
WifiStatusTrackerFactory trackerFactory,
@@ -261,6 +264,7 @@
bgExecutor,
callbackHandler,
accessPointController,
+ statusBarPipelineFlags,
new DataUsageController(context),
new SubscriptionDefaults(),
deviceProvisionedController,
@@ -288,6 +292,7 @@
Executor bgExecutor,
CallbackHandler callbackHandler,
AccessPointControllerImpl accessPointController,
+ StatusBarPipelineFlags statusBarPipelineFlags,
DataUsageController dataUsageController,
SubscriptionDefaults defaultsHandler,
DeviceProvisionedController deviceProvisionedController,
@@ -309,6 +314,7 @@
mBgLooper = bgLooper;
mBgExecutor = bgExecutor;
mCallbackHandler = callbackHandler;
+ mStatusBarPipelineFlags = statusBarPipelineFlags;
mDataSaverController = new DataSaverControllerImpl(context);
mBroadcastDispatcher = broadcastDispatcher;
mMobileFactory = mobileFactory;
@@ -1334,7 +1340,7 @@
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 @@
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 @@
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 c6911b1..a96edf5 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 @@
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 @@
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 4969a1c..6811bf6 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 @@
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 4beb87d..abdf827 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 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 @@
}
}
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 @@
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 fb67f1a..c350c78 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.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 dagger.Binds
import dagger.Module
import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
@Module
abstract class StatusBarPipelineModule {
@@ -52,26 +56,28 @@
@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 da87f73..5479b92 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_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 @@
Connecting(DATA_CONNECTING),
Disconnected(DATA_DISCONNECTED),
Disconnecting(DATA_DISCONNECTING),
+ Unknown(DATA_UNKNOWN),
}
fun @receiver:DataState Int.toDataConnectionType(): DataConnectionState =
@@ -36,5 +38,6 @@
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 581842b..f094563 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 @@
*/
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 c3c1f14..14200f0 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 @@
/** 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 0000000..e214005
--- /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 0000000..5f2feb2
--- /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 0000000..da55787
--- /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 0000000..3f3acaf
--- /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 0000000..4c1cf4a
--- /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 0000000..08d6010
--- /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 c7e0ce1..d9487bf 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.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 @@
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 @@
* 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 162c915..b2ec27c 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.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 @@
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 @@
}
}
- 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 @@
LightRevealScrim(context, null).apply {
revealEffect = createLightRevealEffect()
isScrimOpaqueChangedListener = Consumer {}
- revealAmount = 0f
+ revealAmount = when (reason) {
+ FOLD -> TRANSPARENT
+ UNFOLD -> BLACK
+ }
}
val params = getLayoutParams()
@@ -228,7 +233,7 @@
}
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 @@
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 @@
}
)
+ 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
similarity index 94%
rename from packages/SystemUI/animation/tests/com/android/systemui/animation/InterpolatorsAndroidXTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/animation/InterpolatorsAndroidXTest.kt
index 389eed0..2c680be 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 @@
@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 e7d5632..3c40835 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.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 @@
}
@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 e679b13..d965e33 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.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 @@
@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 @@
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 @@
userFileManager,
userTracker,
Optional.of(taskViewFactory),
+ controlsSettingsRepository,
dumpManager
)
`when`(
@@ -113,11 +143,12 @@
`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 @@
@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/flags/FeatureFlagsDebugRestarterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugRestarterTest.kt
index 1e7b1f2..ed16721 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugRestarterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugRestarterTest.kt
@@ -48,22 +48,22 @@
@Test
fun testRestart_ImmediateWhenAsleep() {
whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP)
- restarter.restart()
- verify(systemExitRestarter).restart()
+ restarter.restartSystemUI()
+ verify(systemExitRestarter).restartSystemUI()
}
@Test
fun testRestart_WaitsForSceenOff() {
whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_AWAKE)
- restarter.restart()
- verify(systemExitRestarter, never()).restart()
+ restarter.restartSystemUI()
+ verify(systemExitRestarter, never()).restartSystemUI()
val captor = ArgumentCaptor.forClass(WakefulnessLifecycle.Observer::class.java)
verify(wakefulnessLifecycle).addObserver(captor.capture())
captor.value.onFinishedGoingToSleep()
- verify(systemExitRestarter).restart()
+ verify(systemExitRestarter).restartSystemUI()
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseRestarterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseRestarterTest.kt
index 68ca48d..7d807e2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseRestarterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseRestarterTest.kt
@@ -63,7 +63,7 @@
whenever(batteryController.isPluggedIn).thenReturn(true)
assertThat(executor.numPending()).isEqualTo(0)
- restarter.restart()
+ restarter.restartSystemUI()
assertThat(executor.numPending()).isEqualTo(1)
}
@@ -72,11 +72,11 @@
whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP)
whenever(batteryController.isPluggedIn).thenReturn(true)
- restarter.restart()
- verify(systemExitRestarter, never()).restart()
+ restarter.restartSystemUI()
+ verify(systemExitRestarter, never()).restartSystemUI()
executor.advanceClockToLast()
executor.runAllReady()
- verify(systemExitRestarter).restart()
+ verify(systemExitRestarter).restartSystemUI()
}
@Test
@@ -85,7 +85,7 @@
whenever(batteryController.isPluggedIn).thenReturn(true)
assertThat(executor.numPending()).isEqualTo(0)
- restarter.restart()
+ restarter.restartSystemUI()
assertThat(executor.numPending()).isEqualTo(0)
}
@@ -95,7 +95,7 @@
whenever(batteryController.isPluggedIn).thenReturn(false)
assertThat(executor.numPending()).isEqualTo(0)
- restarter.restart()
+ restarter.restartSystemUI()
assertThat(executor.numPending()).isEqualTo(0)
}
@@ -105,8 +105,8 @@
whenever(batteryController.isPluggedIn).thenReturn(true)
assertThat(executor.numPending()).isEqualTo(0)
- restarter.restart()
- restarter.restart()
+ restarter.restartSystemUI()
+ restarter.restartSystemUI()
assertThat(executor.numPending()).isEqualTo(1)
}
@@ -115,7 +115,7 @@
whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_AWAKE)
whenever(batteryController.isPluggedIn).thenReturn(true)
assertThat(executor.numPending()).isEqualTo(0)
- restarter.restart()
+ restarter.restartSystemUI()
val captor = ArgumentCaptor.forClass(WakefulnessLifecycle.Observer::class.java)
verify(wakefulnessLifecycle).addObserver(captor.capture())
@@ -131,7 +131,7 @@
whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP)
whenever(batteryController.isPluggedIn).thenReturn(false)
assertThat(executor.numPending()).isEqualTo(0)
- restarter.restart()
+ restarter.restartSystemUI()
val captor =
ArgumentCaptor.forClass(BatteryController.BatteryStateChangeCallback::class.java)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java b/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java
index dc5522e..aa54a1c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java
@@ -23,8 +23,10 @@
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
+import static org.junit.Assert.assertNotEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -171,6 +173,34 @@
}
@Test
+ public void testKeyguardSessionOnDeviceStartsSleepingTwiceInARow_startsNewKeyguardSession()
+ throws RemoteException {
+ // GIVEN session tracker started w/o any sessions
+ mSessionTracker.start();
+ captureKeyguardUpdateMonitorCallback();
+
+ // WHEN device starts going to sleep
+ mKeyguardUpdateMonitorCallback.onStartedGoingToSleep(0);
+
+ // THEN the keyguard session has a session id
+ final InstanceId firstSessionId = mSessionTracker.getSessionId(SESSION_KEYGUARD);
+ assertNotNull(firstSessionId);
+
+ // WHEN device starts going to sleep a second time
+ mKeyguardUpdateMonitorCallback.onStartedGoingToSleep(0);
+
+ // THEN there's a new keyguard session with a unique session id
+ final InstanceId secondSessionId = mSessionTracker.getSessionId(SESSION_KEYGUARD);
+ assertNotNull(secondSessionId);
+ assertNotEquals(firstSessionId, secondSessionId);
+
+ // THEN session start event gets sent to status bar service twice (once per going to
+ // sleep signal)
+ verify(mStatusBarService, times(2)).onSessionStarted(
+ eq(SESSION_KEYGUARD), any(InstanceId.class));
+ }
+
+ @Test
public void testKeyguardSessionOnKeyguardShowingChange() throws RemoteException {
// GIVEN session tracker started w/o any sessions
mSessionTracker.start();
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 d91baa5..80c39cf 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 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.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 @@
.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 faf4592..5431eba 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.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 @@
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 ca75a40..9441d49 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.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 @@
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 84c242c..4c1f0a8 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.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 @@
mFakeExecutor,
mCallbackHandler,
mock(AccessPointControllerImpl.class),
+ mock(StatusBarPipelineFlags.class),
mock(DataUsageController.class),
mMockSubDefaults,
mMockProvisionController,
@@ -115,6 +117,7 @@
mFakeExecutor,
mCallbackHandler,
mock(AccessPointControllerImpl.class),
+ mock(StatusBarPipelineFlags.class),
mock(DataUsageController.class),
mMockSubDefaults,
mMockProvisionController,
@@ -150,6 +153,7 @@
mFakeExecutor,
mCallbackHandler,
mock(AccessPointControllerImpl.class),
+ mock(StatusBarPipelineFlags.class),
mock(DataUsageController.class),
mMockSubDefaults,
mock(DeviceProvisionedController.class),
@@ -188,6 +192,7 @@
mFakeExecutor,
mCallbackHandler,
mock(AccessPointControllerImpl.class),
+ mock(StatusBarPipelineFlags.class),
mock(DataUsageController.class),
mMockSubDefaults,
mock(DeviceProvisionedController.class),
@@ -274,6 +279,7 @@
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 0000000..7eba3b46
--- /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 471f8d3..14a319b 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 @@
@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 @@
@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 @@
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 @@
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 @@
/* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
/* expanded= */ true,
/* tracking= */ false));
- verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
+ verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat());
}
@Test
@@ -304,7 +301,7 @@
/* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
/* expanded= */ true,
/* tracking= */ false));
- verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
+ verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat());
}
@Test
@@ -315,7 +312,7 @@
/* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
/* expanded= */ true,
/* tracking= */ false));
- verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
+ verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat());
}
@Test
@@ -332,7 +329,7 @@
/* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
/* expanded= */ true,
/* tracking= */ false));
- verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
+ verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat());
}
@Test
@@ -343,7 +340,7 @@
/* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
/* expanded= */ true,
/* tracking= */ false));
- verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
+ verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat());
}
@Test
@@ -351,7 +348,7 @@
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 @@
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 @@
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 @@
@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 @@
@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 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 @@
@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 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 @@
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 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 @@
// 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 @@
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 @@
}
@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 0000000..96fba39
--- /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 0000000..96a280a
--- /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 0000000..bf5ecd8
--- /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 0000000..a8f6993
--- /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
similarity index 95%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
index 5ce51bb..c8df5ac 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_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.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 @@
}
@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
similarity index 99%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
index a953a3d..359ea18 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 0000000..01dd60a
--- /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 0000000..4d79554
--- /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 bc083f1..19b5cc9 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.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 @@
}
@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 @@
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 @@
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 e31c952..20f0c17 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 @@
}
@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 @@
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 @@
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 @@
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 @@
}
}
- 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 @@
}
@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 @@
}
if (!skipProxyOperation) {
- finishOperationUnchecked(proxyToken, code, proxyUid, resolvedProxyPackageName,
+ finishOperationUnchecked(clientId, code, proxyUid, resolvedProxyPackageName,
proxyAttributionTag);
}
@@ -4133,7 +4129,7 @@
return null;
}
- finishOperationUnchecked(proxiedToken, code, proxiedUid, resolvedProxiedPackageName,
+ finishOperationUnchecked(clientId, code, proxiedUid, resolvedProxiedPackageName,
proxiedAttributionTag);
return null;
@@ -7726,42 +7722,42 @@
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 @@
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 37538db..d648d6f 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 @@
if (resolvedPackageName == null) {
return;
}
- appOpsManager.finishOp(accessorSource.getToken(), op,
+ appOpsManager.finishOp(attributionSourceState.token, op,
accessorSource.getUid(), resolvedPackageName,
accessorSource.getAttributionTag());
} else {
@@ -1108,8 +1108,9 @@
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 @@
&& 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 @@
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 @@
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 @@
}
@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 @@
if (selfAccess) {
try {
startedOpResult = appOpsManager.startOpNoThrow(
- resolvedAttributionSource.getToken(), startedOp,
+ chainStartToken, startedOp,
resolvedAttributionSource.getUid(),
resolvedAttributionSource.getPackageName(),
/*startIfModeDefault*/ false,
@@ -1575,14 +1578,14 @@
+ " 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 ebd9126..b26c1b9 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.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 @@
}
@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 @@
}
@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 a4c9684..6d5da32 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 @@
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 @@
}
}
+ 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 9957140..e7221c8 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 @@
* 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 5a91dc6..45748e6 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 @@
} 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 76a1122..51eec03 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 @@
}
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 d3d1c16..971b619 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.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 @@
*/
boolean mBuildingFinishLayers = false;
+ private final SurfaceControl.Transaction mWakeT = new SurfaceControl.Transaction();
+
TransitionController(ActivityTaskManagerService atm,
TaskSnapshotController taskSnapshotController,
TransitionTracer transitionTracer) {
@@ -619,8 +622,16 @@
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;