Merge "Create AppList for SpaPrivilegedLib"
diff --git a/api/OWNERS b/api/OWNERS
index 4d8ed03..bf6216c 100644
--- a/api/OWNERS
+++ b/api/OWNERS
@@ -3,7 +3,7 @@
# Modularization team
file:platform/packages/modules/common:/OWNERS
-per-file Android.bp = file:platform/build/soong:/OWNERS
+per-file Android.bp = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION}
# For metalava team to disable lint checks in platform
-per-file Android.bp = aurimas@google.com,emberrose@google.com,sjgilbert@google.com
\ No newline at end of file
+per-file Android.bp = aurimas@google.com,emberrose@google.com,sjgilbert@google.com
diff --git a/config/preloaded-classes-denylist b/config/preloaded-classes-denylist
index 02f2df6..502d8c6 100644
--- a/config/preloaded-classes-denylist
+++ b/config/preloaded-classes-denylist
@@ -9,3 +9,4 @@
android.net.rtp.AudioStream
android.net.rtp.RtpStream
java.util.concurrent.ThreadLocalRandom
+com.android.internal.jank.InteractionJankMonitor$InstanceHolder
diff --git a/core/api/current.txt b/core/api/current.txt
index e043339..795c430 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -52986,6 +52986,22 @@
method public android.view.inputmethod.CursorAnchorInfo.Builder setSelectionRange(int, int);
}
+ public final class DeleteGesture extends android.view.inputmethod.HandwritingGesture implements android.os.Parcelable {
+ method public int describeContents();
+ method @NonNull public android.graphics.RectF getDeletionArea();
+ method public int getGranularity();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.DeleteGesture> CREATOR;
+ }
+
+ public static final class DeleteGesture.Builder {
+ ctor public DeleteGesture.Builder();
+ method @NonNull public android.view.inputmethod.DeleteGesture build();
+ method @NonNull public android.view.inputmethod.DeleteGesture.Builder setDeletionArea(@NonNull android.graphics.RectF);
+ method @NonNull public android.view.inputmethod.DeleteGesture.Builder setFallbackText(@Nullable String);
+ method @NonNull public android.view.inputmethod.DeleteGesture.Builder setGranularity(int);
+ }
+
public final class EditorBoundsInfo implements android.os.Parcelable {
method public int describeContents();
method @Nullable public android.graphics.RectF getEditorBounds();
@@ -53080,6 +53096,12 @@
field public int token;
}
+ public abstract class HandwritingGesture {
+ method @Nullable public String getFallbackText();
+ field public static final int GRANULARITY_CHARACTER = 2; // 0x2
+ field public static final int GRANULARITY_WORD = 1; // 0x1
+ }
+
public final class InlineSuggestion implements android.os.Parcelable {
method public int describeContents();
method @NonNull public android.view.inputmethod.InlineSuggestionInfo getInfo();
@@ -53170,6 +53192,7 @@
method @Nullable public CharSequence getTextBeforeCursor(@IntRange(from=0) int, int);
method public boolean performContextMenuAction(int);
method public boolean performEditorAction(int);
+ method public default void performHandwritingGesture(@NonNull android.view.inputmethod.HandwritingGesture, @Nullable java.util.concurrent.Executor, @Nullable java.util.function.IntConsumer);
method public boolean performPrivateCommand(String, android.os.Bundle);
method public default boolean performSpellCheck();
method public boolean reportFullscreenMode(boolean);
@@ -53391,6 +53414,38 @@
method public android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder setSubtypeNameResId(int);
}
+ public final class InsertGesture extends android.view.inputmethod.HandwritingGesture implements android.os.Parcelable {
+ method public int describeContents();
+ method @Nullable public android.graphics.PointF getInsertionPoint();
+ method @Nullable public String getTextToInsert();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.InsertGesture> CREATOR;
+ }
+
+ public static final class InsertGesture.Builder {
+ ctor public InsertGesture.Builder();
+ method @NonNull public android.view.inputmethod.InsertGesture build();
+ method @NonNull public android.view.inputmethod.InsertGesture.Builder setFallbackText(@Nullable String);
+ method @NonNull public android.view.inputmethod.InsertGesture.Builder setInsertionPoint(@NonNull android.graphics.PointF);
+ method @NonNull public android.view.inputmethod.InsertGesture.Builder setTextToInsert(@NonNull String);
+ }
+
+ public final class SelectGesture extends android.view.inputmethod.HandwritingGesture implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getGranularity();
+ method @NonNull public android.graphics.RectF getSelectionArea();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.SelectGesture> CREATOR;
+ }
+
+ public static final class SelectGesture.Builder {
+ ctor public SelectGesture.Builder();
+ method @NonNull public android.view.inputmethod.SelectGesture build();
+ method @NonNull public android.view.inputmethod.SelectGesture.Builder setFallbackText(@Nullable String);
+ method @NonNull public android.view.inputmethod.SelectGesture.Builder setGranularity(int);
+ method @NonNull public android.view.inputmethod.SelectGesture.Builder setSelectionArea(@NonNull android.graphics.RectF);
+ }
+
public final class SurroundingText implements android.os.Parcelable {
ctor public SurroundingText(@NonNull CharSequence, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0xffffffff) int);
method public int describeContents();
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 25ef6e8..c2b315f 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -980,7 +980,8 @@
boolean mEnterAnimationComplete;
private boolean mIsInMultiWindowMode;
- private boolean mIsInPictureInPictureMode;
+ /** @hide */
+ boolean mIsInPictureInPictureMode;
private boolean mShouldDockBigOverlays;
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index b383d7d..db76816 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -4177,7 +4177,8 @@
private void schedulePauseWithUserLeavingHint(ActivityClientRecord r) {
final ClientTransaction transaction = ClientTransaction.obtain(this.mAppThread, r.token);
transaction.setLifecycleStateRequest(PauseActivityItem.obtain(r.activity.isFinishing(),
- /* userLeaving */ true, r.activity.mConfigChangeFlags, /* dontReport */ false));
+ /* userLeaving */ true, r.activity.mConfigChangeFlags, /* dontReport */ false,
+ /* autoEnteringPip */ false));
executeTransaction(transaction);
}
@@ -4965,12 +4966,18 @@
@Override
public void handlePauseActivity(ActivityClientRecord r, boolean finished, boolean userLeaving,
- int configChanges, PendingTransactionActions pendingActions, String reason) {
+ int configChanges, boolean autoEnteringPip, PendingTransactionActions pendingActions,
+ String reason) {
if (userLeaving) {
performUserLeavingActivity(r);
}
r.activity.mConfigChangeFlags |= configChanges;
+ if (autoEnteringPip) {
+ // Set mIsInPictureInPictureMode earlier in case of auto-enter-pip, see also
+ // {@link Activity#enterPictureInPictureMode(PictureInPictureParams)}.
+ r.activity.mIsInPictureInPictureMode = true;
+ }
performPauseActivity(r, finished, reason, pendingActions);
// Make sure any pending writes are now committed.
diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java
index 389da2d..f322ca9 100644
--- a/core/java/android/app/ClientTransactionHandler.java
+++ b/core/java/android/app/ClientTransactionHandler.java
@@ -96,8 +96,8 @@
/** Pause the activity. */
public abstract void handlePauseActivity(@NonNull ActivityClientRecord r, boolean finished,
- boolean userLeaving, int configChanges, PendingTransactionActions pendingActions,
- String reason);
+ boolean userLeaving, int configChanges, boolean autoEnteringPip,
+ PendingTransactionActions pendingActions, String reason);
/**
* Resume the activity.
diff --git a/core/java/android/app/servertransaction/PauseActivityItem.java b/core/java/android/app/servertransaction/PauseActivityItem.java
index 813e0f9..965e761 100644
--- a/core/java/android/app/servertransaction/PauseActivityItem.java
+++ b/core/java/android/app/servertransaction/PauseActivityItem.java
@@ -39,13 +39,14 @@
private boolean mUserLeaving;
private int mConfigChanges;
private boolean mDontReport;
+ private boolean mAutoEnteringPip;
@Override
public void execute(ClientTransactionHandler client, ActivityClientRecord r,
PendingTransactionActions pendingActions) {
Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityPause");
- client.handlePauseActivity(r, mFinished, mUserLeaving, mConfigChanges, pendingActions,
- "PAUSE_ACTIVITY_ITEM");
+ client.handlePauseActivity(r, mFinished, mUserLeaving, mConfigChanges, mAutoEnteringPip,
+ pendingActions, "PAUSE_ACTIVITY_ITEM");
Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
}
@@ -71,7 +72,7 @@
/** Obtain an instance initialized with provided params. */
public static PauseActivityItem obtain(boolean finished, boolean userLeaving, int configChanges,
- boolean dontReport) {
+ boolean dontReport, boolean autoEnteringPip) {
PauseActivityItem instance = ObjectPool.obtain(PauseActivityItem.class);
if (instance == null) {
instance = new PauseActivityItem();
@@ -80,6 +81,7 @@
instance.mUserLeaving = userLeaving;
instance.mConfigChanges = configChanges;
instance.mDontReport = dontReport;
+ instance.mAutoEnteringPip = autoEnteringPip;
return instance;
}
@@ -94,6 +96,7 @@
instance.mUserLeaving = false;
instance.mConfigChanges = 0;
instance.mDontReport = true;
+ instance.mAutoEnteringPip = false;
return instance;
}
@@ -105,6 +108,7 @@
mUserLeaving = false;
mConfigChanges = 0;
mDontReport = false;
+ mAutoEnteringPip = false;
ObjectPool.recycle(this);
}
@@ -117,6 +121,7 @@
dest.writeBoolean(mUserLeaving);
dest.writeInt(mConfigChanges);
dest.writeBoolean(mDontReport);
+ dest.writeBoolean(mAutoEnteringPip);
}
/** Read from Parcel. */
@@ -125,6 +130,7 @@
mUserLeaving = in.readBoolean();
mConfigChanges = in.readInt();
mDontReport = in.readBoolean();
+ mAutoEnteringPip = in.readBoolean();
}
public static final @NonNull Creator<PauseActivityItem> CREATOR =
@@ -148,7 +154,8 @@
}
final PauseActivityItem other = (PauseActivityItem) o;
return mFinished == other.mFinished && mUserLeaving == other.mUserLeaving
- && mConfigChanges == other.mConfigChanges && mDontReport == other.mDontReport;
+ && mConfigChanges == other.mConfigChanges && mDontReport == other.mDontReport
+ && mAutoEnteringPip == other.mAutoEnteringPip;
}
@Override
@@ -158,12 +165,14 @@
result = 31 * result + (mUserLeaving ? 1 : 0);
result = 31 * result + mConfigChanges;
result = 31 * result + (mDontReport ? 1 : 0);
+ result = 31 * result + (mAutoEnteringPip ? 1 : 0);
return result;
}
@Override
public String toString() {
return "PauseActivityItem{finished=" + mFinished + ",userLeaving=" + mUserLeaving
- + ",configChanges=" + mConfigChanges + ",dontReport=" + mDontReport + "}";
+ + ",configChanges=" + mConfigChanges + ",dontReport=" + mDontReport
+ + ",autoEnteringPip=" + mAutoEnteringPip + "}";
}
}
diff --git a/core/java/android/app/servertransaction/TransactionExecutor.java b/core/java/android/app/servertransaction/TransactionExecutor.java
index 25ff8a7..de1d38a 100644
--- a/core/java/android/app/servertransaction/TransactionExecutor.java
+++ b/core/java/android/app/servertransaction/TransactionExecutor.java
@@ -227,7 +227,8 @@
break;
case ON_PAUSE:
mTransactionHandler.handlePauseActivity(r, false /* finished */,
- false /* userLeaving */, 0 /* configChanges */, mPendingActions,
+ false /* userLeaving */, 0 /* configChanges */,
+ false /* autoEnteringPip */, mPendingActions,
"LIFECYCLER_PAUSE_ACTIVITY");
break;
case ON_STOP:
diff --git a/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java b/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java
index e38e611..3260713 100644
--- a/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java
+++ b/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java
@@ -17,6 +17,7 @@
package android.inputmethodservice;
import android.annotation.AnyThread;
+import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Bundle;
@@ -24,9 +25,13 @@
import android.view.KeyEvent;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.DeleteGesture;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.HandwritingGesture;
import android.view.inputmethod.InputContentInfo;
+import android.view.inputmethod.InsertGesture;
+import android.view.inputmethod.SelectGesture;
import android.view.inputmethod.SurroundingText;
import android.view.inputmethod.TextAttribute;
@@ -35,6 +40,8 @@
import com.android.internal.inputmethod.InputConnectionCommandHeader;
import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.IntConsumer;
/**
* A stateless wrapper of {@link com.android.internal.inputmethod.IRemoteInputConnection} to
@@ -591,6 +598,27 @@
}
}
+ @AnyThread
+ public void performHandwritingGesture(
+ @NonNull HandwritingGesture gesture, @Nullable @CallbackExecutor Executor executor,
+ @Nullable IntConsumer consumer) {
+ // TODO(b/210039666): implement resultReceiver
+ try {
+ if (gesture instanceof SelectGesture) {
+ mConnection.performHandwritingSelectGesture(
+ createHeader(), (SelectGesture) gesture, null);
+ } else if (gesture instanceof InsertGesture) {
+ mConnection.performHandwritingInsertGesture(
+ createHeader(), (InsertGesture) gesture, null);
+ } else if (gesture instanceof DeleteGesture) {
+ mConnection.performHandwritingDeleteGesture(
+ createHeader(), (DeleteGesture) gesture, null);
+ }
+ } catch (RemoteException e) {
+ // TODO(b/210039666): return result
+ }
+ }
+
/**
* Invokes {@link IRemoteInputConnection#requestCursorUpdates(InputConnectionCommandHeader, int,
* int, AndroidFuture)}.
diff --git a/core/java/android/inputmethodservice/RemoteInputConnection.java b/core/java/android/inputmethodservice/RemoteInputConnection.java
index 2711c4f..694293c 100644
--- a/core/java/android/inputmethodservice/RemoteInputConnection.java
+++ b/core/java/android/inputmethodservice/RemoteInputConnection.java
@@ -17,6 +17,7 @@
package android.inputmethodservice;
import android.annotation.AnyThread;
+import android.annotation.CallbackExecutor;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -28,6 +29,7 @@
import android.view.inputmethod.CorrectionInfo;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.HandwritingGesture;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputContentInfo;
import android.view.inputmethod.SurroundingText;
@@ -41,6 +43,8 @@
import java.lang.ref.WeakReference;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.function.IntConsumer;
/**
* Takes care of remote method invocations of {@link InputConnection} in the IME side.
@@ -411,6 +415,13 @@
}
@AnyThread
+ public void performHandwritingGesture(
+ @NonNull HandwritingGesture gesture, @Nullable @CallbackExecutor Executor executor,
+ @Nullable IntConsumer consumer) {
+ mInvoker.performHandwritingGesture(gesture, executor, consumer);
+ }
+
+ @AnyThread
public boolean requestCursorUpdates(int cursorUpdateMode) {
if (mCancellationGroup.isCanceled()) {
return false;
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index ca0af2c..738f9c9 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -1995,7 +1995,8 @@
/** @hide */
public UserManager(Context context, IUserManager service) {
mService = service;
- mContext = context.getApplicationContext();
+ Context appContext = context.getApplicationContext();
+ mContext = (appContext == null ? context : appContext);
mUserId = context.getUserId();
}
diff --git a/core/java/android/service/selectiontoolbar/RemoteSelectionToolbar.java b/core/java/android/service/selectiontoolbar/RemoteSelectionToolbar.java
index 95bcda5..9292e96 100644
--- a/core/java/android/service/selectiontoolbar/RemoteSelectionToolbar.java
+++ b/core/java/android/service/selectiontoolbar/RemoteSelectionToolbar.java
@@ -1317,7 +1317,6 @@
contentContainer.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
contentContainer.setTag(FloatingToolbar.FLOATING_TOOLBAR_TAG);
- contentContainer.setContentDescription(FloatingToolbar.FLOATING_TOOLBAR_TAG);
contentContainer.setClipToOutline(true);
return contentContainer;
}
diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java
index b73ff901..0338ceb 100644
--- a/core/java/android/util/FeatureFlagUtils.java
+++ b/core/java/android/util/FeatureFlagUtils.java
@@ -97,10 +97,6 @@
/** @hide */
public static final String SETTINGS_AUTO_TEXT_WRAPPING = "settings_auto_text_wrapping";
- /** Flag to enable/disable guest mode UX changes as mentioned in b/214031645
- * @hide
- */
- public static final String SETTINGS_GUEST_MODE_UX_CHANGES = "settings_guest_mode_ux_changes";
/** Support Clear Calling feature.
* @hide
@@ -150,7 +146,6 @@
DEFAULT_FLAGS.put(SETTINGS_APP_ALLOW_DARK_THEME_ACTIVATION_AT_BEDTIME, "true");
DEFAULT_FLAGS.put(SETTINGS_HIDE_SECOND_LAYER_PAGE_NAVIGATE_UP_BUTTON_IN_TWO_PANE, "true");
DEFAULT_FLAGS.put(SETTINGS_AUTO_TEXT_WRAPPING, "false");
- DEFAULT_FLAGS.put(SETTINGS_GUEST_MODE_UX_CHANGES, "true");
DEFAULT_FLAGS.put(SETTINGS_ENABLE_CLEAR_CALLING, "false");
DEFAULT_FLAGS.put(SETTINGS_ACCESSIBILITY_SIMPLE_CURSOR, "false");
DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_UI, "false");
diff --git a/core/java/android/view/inputmethod/DeleteGesture.aidl b/core/java/android/view/inputmethod/DeleteGesture.aidl
new file mode 100644
index 0000000..e9f31dd
--- /dev/null
+++ b/core/java/android/view/inputmethod/DeleteGesture.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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 android.view.inputmethod;
+
+parcelable DeleteGesture;
\ No newline at end of file
diff --git a/core/java/android/view/inputmethod/DeleteGesture.java b/core/java/android/view/inputmethod/DeleteGesture.java
new file mode 100644
index 0000000..257254e
--- /dev/null
+++ b/core/java/android/view/inputmethod/DeleteGesture.java
@@ -0,0 +1,193 @@
+/*
+ * 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 android.view.inputmethod;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.graphics.RectF;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.widget.TextView;
+
+import java.util.Objects;
+
+/**
+ * A sub-class of {@link HandwritingGesture} for deleting an area of text.
+ * This class holds the information required for deletion of text in
+ * toolkit widgets like {@link TextView}.
+ */
+public final class DeleteGesture extends HandwritingGesture implements Parcelable {
+
+ private @Granularity int mGranularity;
+ private RectF mArea;
+
+ private DeleteGesture(@Granularity int granularity, RectF area, String fallbackText) {
+ mArea = area;
+ mGranularity = granularity;
+ mFallbackText = fallbackText;
+ }
+
+ private DeleteGesture(@NonNull final Parcel source) {
+ mFallbackText = source.readString8();
+ mGranularity = source.readInt();
+ mArea = source.readTypedObject(RectF.CREATOR);
+ }
+
+ /**
+ * Returns Granular level on which text should be operated.
+ * @see HandwritingGesture#GRANULARITY_CHARACTER
+ * @see HandwritingGesture#GRANULARITY_WORD
+ */
+ @Granularity
+ public int getGranularity() {
+ return mGranularity;
+ }
+
+ /**
+ * Returns the deletion area {@link RectF} in screen coordinates.
+ *
+ * Getter for deletion area set with {@link DeleteGesture.Builder#setDeletionArea(RectF)}.
+ * {@code null} if area was not set.
+ */
+ @NonNull
+ public RectF getDeletionArea() {
+ return mArea;
+ }
+
+ /**
+ * Builder for {@link DeleteGesture}. This class is not designed to be thread-safe.
+ */
+ public static final class Builder {
+ private int mGranularity;
+ private RectF mArea;
+ private String mFallbackText;
+
+ /**
+ * Set text deletion granularity. Intersecting words/characters will be
+ * included in the operation.
+ * @param granularity {@link HandwritingGesture#GRANULARITY_WORD} or
+ * {@link HandwritingGesture#GRANULARITY_CHARACTER}.
+ * @return {@link Builder}.
+ */
+ @NonNull
+ @SuppressLint("MissingGetterMatchingBuilder")
+ public Builder setGranularity(@Granularity int granularity) {
+ mGranularity = granularity;
+ return this;
+ }
+
+ /**
+ * Set rectangular single/multiline text deletion area intersecting with text.
+ *
+ * The resulting deletion would be performed for all text intersecting rectangle. The
+ * deletion includes the first word/character in the rectangle, and the last
+ * word/character in the rectangle, and includes everything in between even if it's not
+ * in the rectangle.
+ *
+ * Intersection is determined using
+ * {@link #setGranularity(int)}. e.g. {@link HandwritingGesture#GRANULARITY_WORD} includes
+ * all the words with their width/height center included in the deletion rectangle.
+ * @param area {@link RectF} (in screen coordinates) for which text will be deleted.
+ * @see HandwritingGesture#GRANULARITY_WORD
+ * @see HandwritingGesture#GRANULARITY_CHARACTER
+ */
+ @SuppressLint("MissingGetterMatchingBuilder")
+ @NonNull
+ public Builder setDeletionArea(@NonNull RectF area) {
+ mArea = area;
+ return this;
+ }
+
+ /**
+ * Set fallback text that will be committed at current cursor position if there is no
+ * applicable text beneath the area of gesture.
+ * @param fallbackText text to set
+ */
+ @NonNull
+ public Builder setFallbackText(@Nullable String fallbackText) {
+ mFallbackText = fallbackText;
+ return this;
+ }
+
+ /**
+ * @return {@link DeleteGesture} using parameters in this {@link DeleteGesture.Builder}.
+ * @throws IllegalArgumentException if one or more positional parameters are not specified.
+ */
+ @NonNull
+ public DeleteGesture build() {
+ if (mArea == null || mArea.isEmpty()) {
+ throw new IllegalArgumentException("Deletion area must be set.");
+ }
+ if (mGranularity <= GRANULARITY_UNDEFINED) {
+ throw new IllegalArgumentException("Deletion granularity must be set.");
+ }
+ return new DeleteGesture(mGranularity, mArea, mFallbackText);
+ }
+ }
+
+ /**
+ * Used to make this class parcelable.
+ */
+ public static final @android.annotation.NonNull Creator<DeleteGesture> CREATOR =
+ new Creator<DeleteGesture>() {
+ @Override
+ public DeleteGesture createFromParcel(Parcel source) {
+ return new DeleteGesture(source);
+ }
+
+ @Override
+ public DeleteGesture[] newArray(int size) {
+ return new DeleteGesture[size];
+ }
+ };
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mArea, mGranularity, mFallbackText);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof DeleteGesture)) return false;
+
+ DeleteGesture that = (DeleteGesture) o;
+
+ if (mGranularity != that.mGranularity) return false;
+ if (!Objects.equals(mFallbackText, that.mFallbackText)) return false;
+ return Objects.equals(mArea, that.mArea);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Used to package this object into a {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to be written.
+ * @param flags The flags used for parceling.
+ */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeString8(mFallbackText);
+ dest.writeInt(mGranularity);
+ dest.writeTypedObject(mArea, flags);
+ }
+}
diff --git a/core/java/android/view/inputmethod/HandwritingGesture.java b/core/java/android/view/inputmethod/HandwritingGesture.java
new file mode 100644
index 0000000..15824ae
--- /dev/null
+++ b/core/java/android/view/inputmethod/HandwritingGesture.java
@@ -0,0 +1,94 @@
+/*
+ * 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 android.view.inputmethod;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.graphics.RectF;
+import android.inputmethodservice.InputMethodService;
+import android.view.MotionEvent;
+
+import java.util.concurrent.Executor;
+import java.util.function.IntConsumer;
+
+/**
+ * Base class for Stylus handwriting gesture.
+ *
+ * During a stylus handwriting session, user can perform a stylus gesture operation like
+ * {@link SelectGesture}, {@link DeleteGesture}, {@link InsertGesture} on an
+ * area of text. IME is responsible for listening to Stylus {@link MotionEvent} using
+ * {@link InputMethodService#onStylusHandwritingMotionEvent} and interpret if it can translate to a
+ * gesture operation.
+ * While creating Gesture operations {@link SelectGesture}, {@link DeleteGesture},
+ * , {@code Granularity} helps pick the correct granular level of text like word level
+ * {@link #GRANULARITY_WORD}, or character level {@link #GRANULARITY_CHARACTER}.
+ *
+ * @see InputConnection#performHandwritingGesture(HandwritingGesture, Executor, IntConsumer)
+ * @see InputMethodService#onStartStylusHandwriting()
+ */
+public abstract class HandwritingGesture {
+
+ HandwritingGesture() {}
+
+ static final int GRANULARITY_UNDEFINED = 0;
+
+ /**
+ * Operate text per word basis. e.g. if selection includes width-wise center of the word,
+ * whole word is selected.
+ * <p> Strategy of operating at a granular level is maintained in the UI toolkit.
+ * A character/word/line is included if its center is within the gesture rectangle.
+ * e.g. if a selection {@link RectF} with {@link #GRANULARITY_WORD} includes width-wise
+ * center of the word, it should be selected.
+ * Similarly, text in a line should be included in the operation if rectangle includes
+ * line height center.</p>
+ * Refer to https://www.unicode.org/reports/tr29/#Word_Boundaries for more detail on how word
+ * breaks are decided.
+ */
+ public static final int GRANULARITY_WORD = 1;
+
+ /**
+ * Operate on text per character basis. i.e. each character is selected based on its
+ * intersection with selection rectangle.
+ * <p> Strategy of operating at a granular level is maintained in the UI toolkit.
+ * A character/word/line is included if its center is within the gesture rectangle.
+ * e.g. if a selection {@link RectF} with {@link #GRANULARITY_CHARACTER} includes width-wise
+ * center of the character, it should be selected.
+ * Similarly, text in a line should be included in the operation if rectangle includes
+ * line height center.</p>
+ */
+ public static final int GRANULARITY_CHARACTER = 2;
+
+ /**
+ * Granular level on which text should be operated.
+ */
+ @IntDef({GRANULARITY_CHARACTER, GRANULARITY_WORD})
+ @interface Granularity {}
+
+ @Nullable
+ String mFallbackText;
+
+ /**
+ * The fallback text that will be committed at current cursor position if there is no applicable
+ * text beneath the area of gesture.
+ * For example, select can fail if gesture is drawn over area that has no text beneath.
+ * example 2: join can fail if the gesture is drawn over text but there is no whitespace.
+ */
+ @Nullable
+ public String getFallbackText() {
+ return mFallbackText;
+ }
+}
diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java
index dac1be6..7b0270a1 100644
--- a/core/java/android/view/inputmethod/InputConnection.java
+++ b/core/java/android/view/inputmethod/InputConnection.java
@@ -16,6 +16,7 @@
package android.view.inputmethod;
+import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
@@ -31,6 +32,8 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.Executor;
+import java.util.function.IntConsumer;
/**
* The InputConnection interface is the communication channel from an
@@ -968,6 +971,17 @@
boolean performPrivateCommand(String action, Bundle data);
/**
+ * Perform a handwriting gesture on text.
+ *
+ * @param gesture the gesture to perform
+ * @param executor if the caller passes a non-null consumer TODO(b/210039666): complete doc
+ * @param consumer if the caller passes a non-null receiver, the editor must invoke this
+ */
+ default void performHandwritingGesture(
+ @NonNull HandwritingGesture gesture, @Nullable @CallbackExecutor Executor executor,
+ @Nullable IntConsumer consumer) {}
+
+ /**
* The editor is requested to call
* {@link InputMethodManager#updateCursorAnchorInfo(android.view.View, CursorAnchorInfo)} at
* once, as soon as possible, regardless of cursor/anchor position changes. This flag can be
diff --git a/core/java/android/view/inputmethod/InputConnectionWrapper.java b/core/java/android/view/inputmethod/InputConnectionWrapper.java
index 7a88a75..56beddf 100644
--- a/core/java/android/view/inputmethod/InputConnectionWrapper.java
+++ b/core/java/android/view/inputmethod/InputConnectionWrapper.java
@@ -16,6 +16,7 @@
package android.view.inputmethod;
+import android.annotation.CallbackExecutor;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -25,6 +26,9 @@
import com.android.internal.util.Preconditions;
+import java.util.concurrent.Executor;
+import java.util.function.IntConsumer;
+
/**
* <p>Wrapper class for proxying calls to another InputConnection. Subclass and have fun!
*/
@@ -323,6 +327,17 @@
* @throws NullPointerException if the target is {@code null}.
*/
@Override
+ public void performHandwritingGesture(
+ @NonNull HandwritingGesture gesture, @Nullable @CallbackExecutor Executor executor,
+ @Nullable IntConsumer consumer) {
+ mTarget.performHandwritingGesture(gesture, executor, consumer);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
public boolean requestCursorUpdates(int cursorUpdateMode) {
return mTarget.requestCursorUpdates(cursorUpdateMode);
}
diff --git a/core/java/android/view/inputmethod/InsertGesture.aidl b/core/java/android/view/inputmethod/InsertGesture.aidl
new file mode 100644
index 0000000..9cdb14a
--- /dev/null
+++ b/core/java/android/view/inputmethod/InsertGesture.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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 android.view.inputmethod;
+
+parcelable InsertGesture;
\ No newline at end of file
diff --git a/core/java/android/view/inputmethod/InsertGesture.java b/core/java/android/view/inputmethod/InsertGesture.java
new file mode 100644
index 0000000..2cf015a
--- /dev/null
+++ b/core/java/android/view/inputmethod/InsertGesture.java
@@ -0,0 +1,172 @@
+/*
+ * 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 android.view.inputmethod;
+
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.graphics.PointF;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+
+import java.util.Objects;
+
+/**
+ * A sub-class of {@link HandwritingGesture} for inserting text at the defined insertion point.
+ * This class holds the information required for insertion of text in
+ * toolkit widgets like {@link TextView}.
+ */
+public final class InsertGesture extends HandwritingGesture implements Parcelable {
+
+ private String mTextToInsert;
+ private PointF mPoint;
+
+ private InsertGesture(String text, PointF point, String fallbackText) {
+ mPoint = point;
+ mTextToInsert = text;
+ mFallbackText = fallbackText;
+ }
+
+ private InsertGesture(final Parcel source) {
+ mFallbackText = source.readString8();
+ mTextToInsert = source.readString8();
+ mPoint = source.readTypedObject(PointF.CREATOR);
+ }
+
+ /** Returns the text that will be inserted at {@link #getInsertionPoint()} **/
+ @Nullable
+ public String getTextToInsert() {
+ return mTextToInsert;
+ }
+
+ /**
+ * Returns the insertion point {@link PointF} (in screen coordinates) where
+ * {@link #getTextToInsert()} will be inserted.
+ */
+ @Nullable
+ public PointF getInsertionPoint() {
+ return mPoint;
+ }
+
+ /**
+ * Builder for {@link InsertGesture}. This class is not designed to be thread-safe.
+ */
+ public static final class Builder {
+ private String mText;
+ private PointF mPoint;
+ private String mFallbackText;
+
+ /** set the text that will be inserted at {@link #setInsertionPoint(PointF)} **/
+ @NonNull
+ @SuppressLint("MissingGetterMatchingBuilder")
+ public Builder setTextToInsert(@NonNull String text) {
+ mText = text;
+ return this;
+ }
+
+ /**
+ * Sets the insertion point (in screen coordinates) where {@link #setTextToInsert(String)}
+ * should be inserted.
+ */
+ @NonNull
+ @SuppressLint("MissingGetterMatchingBuilder")
+ public Builder setInsertionPoint(@NonNull PointF point) {
+ mPoint = point;
+ return this;
+ }
+
+ /**
+ * Set fallback text that will be committed at current cursor position if there is no
+ * applicable text beneath the area of gesture.
+ * @param fallbackText text to set
+ */
+ @NonNull
+ public Builder setFallbackText(@Nullable String fallbackText) {
+ mFallbackText = fallbackText;
+ return this;
+ }
+
+ /**
+ * @return {@link InsertGesture} using parameters in this {@link InsertGesture.Builder}.
+ * @throws IllegalArgumentException if one or more positional parameters are not specified.
+ */
+ @NonNull
+ public InsertGesture build() {
+ if (mPoint == null) {
+ throw new IllegalArgumentException("Insertion point must be set.");
+ }
+ if (TextUtils.isEmpty(mText)) {
+ throw new IllegalArgumentException("Text to insert must be non-empty.");
+ }
+ return new InsertGesture(mText, mPoint, mFallbackText);
+ }
+ }
+
+ /**
+ * Used to make this class parcelable.
+ */
+ public static final @android.annotation.NonNull Creator<InsertGesture> CREATOR =
+ new Creator<InsertGesture>() {
+ @Override
+ public InsertGesture createFromParcel(Parcel source) {
+ return new InsertGesture(source);
+ }
+
+ @Override
+ public InsertGesture[] newArray(int size) {
+ return new InsertGesture[size];
+ }
+ };
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mPoint, mTextToInsert, mFallbackText);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof InsertGesture)) return false;
+
+ InsertGesture that = (InsertGesture) o;
+
+ if (!Objects.equals(mFallbackText, that.mFallbackText)) return false;
+ if (!Objects.equals(mTextToInsert, that.mTextToInsert)) return false;
+ return Objects.equals(mPoint, that.mPoint);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Used to package this object into a {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to be written.
+ * @param flags The flags used for parceling.
+ */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeString8(mFallbackText);
+ dest.writeString8(mTextToInsert);
+ dest.writeTypedObject(mPoint, flags);
+ }
+}
diff --git a/core/java/android/view/inputmethod/SelectGesture.aidl b/core/java/android/view/inputmethod/SelectGesture.aidl
new file mode 100644
index 0000000..65da4f3
--- /dev/null
+++ b/core/java/android/view/inputmethod/SelectGesture.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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 android.view.inputmethod;
+
+parcelable SelectGesture;
\ No newline at end of file
diff --git a/core/java/android/view/inputmethod/SelectGesture.java b/core/java/android/view/inputmethod/SelectGesture.java
new file mode 100644
index 0000000..f3cd71e
--- /dev/null
+++ b/core/java/android/view/inputmethod/SelectGesture.java
@@ -0,0 +1,191 @@
+/*
+ * 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 android.view.inputmethod;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.graphics.RectF;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.widget.TextView;
+
+import java.util.Objects;
+
+/**
+ * A sub-class of {@link HandwritingGesture} for selecting an area of text.
+ * This class holds the information required for selection of text in
+ * toolkit widgets like {@link TextView}.
+ */
+public final class SelectGesture extends HandwritingGesture implements Parcelable {
+
+ private @Granularity int mGranularity;
+ private RectF mArea;
+
+ private SelectGesture(int granularity, RectF area, String fallbackText) {
+ mArea = area;
+ mGranularity = granularity;
+ mFallbackText = fallbackText;
+ }
+
+ private SelectGesture(@NonNull Parcel source) {
+ mFallbackText = source.readString8();
+ mGranularity = source.readInt();
+ mArea = source.readTypedObject(RectF.CREATOR);
+ }
+
+ /**
+ * Returns Granular level on which text should be operated.
+ * @see #GRANULARITY_CHARACTER
+ * @see #GRANULARITY_WORD
+ */
+ @Granularity
+ public int getGranularity() {
+ return mGranularity;
+ }
+
+ /**
+ * Returns the Selection area {@link RectF} in screen coordinates.
+ *
+ * Getter for selection area set with {@link Builder#setSelectionArea(RectF)}. {@code null}
+ * if area was not set.
+ */
+ @NonNull
+ public RectF getSelectionArea() {
+ return mArea;
+ }
+
+
+ /**
+ * Builder for {@link SelectGesture}. This class is not designed to be thread-safe.
+ */
+ public static final class Builder {
+ private int mGranularity;
+ private RectF mArea;
+ private String mFallbackText;
+
+ /**
+ * Define text selection granularity. Intersecting words/characters will be
+ * included in the operation.
+ * @param granularity {@link HandwritingGesture#GRANULARITY_WORD} or
+ * {@link HandwritingGesture#GRANULARITY_CHARACTER}.
+ * @return {@link Builder}.
+ */
+ @NonNull
+ @SuppressLint("MissingGetterMatchingBuilder")
+ public Builder setGranularity(@Granularity int granularity) {
+ mGranularity = granularity;
+ return this;
+ }
+
+ /**
+ * Set rectangular single/multiline text selection area intersecting with text.
+ *
+ * The resulting selection would be performed for all text intersecting rectangle. The
+ * selection includes the first word/character in the rectangle, and the last
+ * word/character in the rectangle, and includes everything in between even if it's not
+ * in the rectangle.
+ *
+ * Intersection is determined using
+ * {@link #setGranularity(int)}. e.g. {@link HandwritingGesture#GRANULARITY_WORD} includes
+ * all the words with their width/height center included in the selection rectangle.
+ * @param area {@link RectF} (in screen coordinates) for which text will be selection.
+ */
+ @NonNull
+ @SuppressLint("MissingGetterMatchingBuilder")
+ public Builder setSelectionArea(@NonNull RectF area) {
+ mArea = area;
+ return this;
+ }
+
+ /**
+ * Set fallback text that will be committed at current cursor position if there is no
+ * applicable text beneath the area of gesture.
+ * @param fallbackText text to set
+ */
+ @NonNull
+ public Builder setFallbackText(@Nullable String fallbackText) {
+ mFallbackText = fallbackText;
+ return this;
+ }
+
+ /**
+ * @return {@link SelectGesture} using parameters in this {@link InsertGesture.Builder}.
+ * @throws IllegalArgumentException if one or more positional parameters are not specified.
+ */
+ @NonNull
+ public SelectGesture build() {
+ if (mArea == null || mArea.isEmpty()) {
+ throw new IllegalArgumentException("Selection area must be set.");
+ }
+ if (mGranularity <= GRANULARITY_UNDEFINED) {
+ throw new IllegalArgumentException("Selection granularity must be set.");
+ }
+ return new SelectGesture(mGranularity, mArea, mFallbackText);
+ }
+ }
+
+ /**
+ * Used to make this class parcelable.
+ */
+ public static final @android.annotation.NonNull Parcelable.Creator<SelectGesture> CREATOR =
+ new Parcelable.Creator<SelectGesture>() {
+ @Override
+ public SelectGesture createFromParcel(Parcel source) {
+ return new SelectGesture(source);
+ }
+
+ @Override
+ public SelectGesture[] newArray(int size) {
+ return new SelectGesture[size];
+ }
+ };
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mGranularity, mArea, mFallbackText);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof SelectGesture)) return false;
+
+ SelectGesture that = (SelectGesture) o;
+
+ if (mGranularity != that.mGranularity) return false;
+ if (!Objects.equals(mFallbackText, that.mFallbackText)) return false;
+ return Objects.equals(mArea, that.mArea);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Used to package this object into a {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to be written.
+ * @param flags The flags used for parceling.
+ */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeString8(mFallbackText);
+ dest.writeInt(mGranularity);
+ dest.writeTypedObject(mArea, flags);
+ }
+}
diff --git a/core/java/android/window/ITaskFragmentOrganizerController.aidl b/core/java/android/window/ITaskFragmentOrganizerController.aidl
index 8407d10..884ca77 100644
--- a/core/java/android/window/ITaskFragmentOrganizerController.aidl
+++ b/core/java/android/window/ITaskFragmentOrganizerController.aidl
@@ -16,8 +16,10 @@
package android.window;
+import android.os.IBinder;
import android.view.RemoteAnimationDefinition;
import android.window.ITaskFragmentOrganizer;
+import android.window.WindowContainerTransaction;
/** @hide */
interface ITaskFragmentOrganizerController {
@@ -46,8 +48,15 @@
void unregisterRemoteAnimations(in ITaskFragmentOrganizer organizer, int taskId);
/**
- * Checks if an activity organized by a {@link android.window.TaskFragmentOrganizer} and
- * only occupies a portion of Task bounds.
- */
+ * Checks if an activity organized by a {@link android.window.TaskFragmentOrganizer} and
+ * only occupies a portion of Task bounds.
+ */
boolean isActivityEmbedded(in IBinder activityToken);
+
+ /**
+ * Notifies the server that the organizer has finished handling the given transaction. The
+ * server should apply the given {@link WindowContainerTransaction} for the necessary changes.
+ */
+ void onTransactionHandled(in ITaskFragmentOrganizer organizer, in IBinder transactionToken,
+ in WindowContainerTransaction wct);
}
diff --git a/core/java/android/window/TaskFragmentOrganizer.java b/core/java/android/window/TaskFragmentOrganizer.java
index cd15df84..7359172 100644
--- a/core/java/android/window/TaskFragmentOrganizer.java
+++ b/core/java/android/window/TaskFragmentOrganizer.java
@@ -26,7 +26,6 @@
import android.annotation.CallSuper;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.SuppressLint;
import android.annotation.TestApi;
import android.content.Intent;
import android.content.res.Configuration;
@@ -141,6 +140,28 @@
}
/**
+ * Notifies the server that the organizer has finished handling the given transaction. The
+ * server should apply the given {@link WindowContainerTransaction} for the necessary changes.
+ *
+ * @param transactionToken {@link TaskFragmentTransaction#getTransactionToken()} from
+ * {@link #onTransactionReady(TaskFragmentTransaction)}
+ * @param wct {@link WindowContainerTransaction} that the server should apply for
+ * update of the transaction.
+ * @see com.android.server.wm.WindowOrganizerController#enforceTaskPermission for permission
+ * requirement.
+ * @hide
+ */
+ public void onTransactionHandled(@NonNull IBinder transactionToken,
+ @NonNull WindowContainerTransaction wct) {
+ wct.setTaskFragmentOrganizer(mInterface);
+ try {
+ getController().onTransactionHandled(mInterface, transactionToken, wct);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Called when a TaskFragment is created and organized by this organizer.
*
* @param wct The {@link WindowContainerTransaction} to make any changes with if needed. No
@@ -227,12 +248,8 @@
/**
* Called when the transaction is ready so that the organizer can update the TaskFragments based
* on the changes in transaction.
- * Note: {@link WindowOrganizer#applyTransaction} permission requirement is conditional for
- * {@link TaskFragmentOrganizer}.
- * @see com.android.server.wm.WindowOrganizerController#enforceTaskPermission
* @hide
*/
- @SuppressLint("AndroidFrameworkRequiresPermission")
public void onTransactionReady(@NonNull TaskFragmentTransaction transaction) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
@@ -274,8 +291,9 @@
"Unknown TaskFragmentEvent=" + change.getType());
}
}
- // TODO(b/240519866): notify TaskFragmentOrganizerController that the transition is done.
- applyTransaction(wct);
+
+ // Notify the server, and the server should apply the WindowContainerTransaction.
+ onTransactionHandled(transaction.getTransactionToken(), wct);
}
@Override
diff --git a/core/java/android/window/TaskFragmentTransaction.java b/core/java/android/window/TaskFragmentTransaction.java
index 07e8e8c..84a5fea 100644
--- a/core/java/android/window/TaskFragmentTransaction.java
+++ b/core/java/android/window/TaskFragmentTransaction.java
@@ -23,6 +23,7 @@
import android.annotation.Nullable;
import android.content.Intent;
import android.content.res.Configuration;
+import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcel;
@@ -41,19 +42,31 @@
*/
public final class TaskFragmentTransaction implements Parcelable {
+ /** Unique token to represent this transaction. */
+ private final IBinder mTransactionToken;
+
+ /** Changes in this transaction. */
private final ArrayList<Change> mChanges = new ArrayList<>();
- public TaskFragmentTransaction() {}
+ public TaskFragmentTransaction() {
+ mTransactionToken = new Binder();
+ }
private TaskFragmentTransaction(Parcel in) {
+ mTransactionToken = in.readStrongBinder();
in.readTypedList(mChanges, Change.CREATOR);
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeStrongBinder(mTransactionToken);
dest.writeTypedList(mChanges);
}
+ public IBinder getTransactionToken() {
+ return mTransactionToken;
+ }
+
/** Adds a {@link Change} to this transaction. */
public void addChange(@Nullable Change change) {
if (change != null) {
@@ -74,7 +87,9 @@
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
- sb.append("TaskFragmentTransaction{changes=[");
+ sb.append("TaskFragmentTransaction{token=");
+ sb.append(mTransactionToken);
+ sb.append(" changes=[");
for (int i = 0; i < mChanges.size(); ++i) {
if (i > 0) {
sb.append(',');
diff --git a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
index fcdcb2d..8f6bc43 100644
--- a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
+++ b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
@@ -217,7 +217,11 @@
case TYPE_HEADER_ALL_OTHERS:
TextView textView = (TextView) itemView;
if (itemType == TYPE_HEADER_SUGGESTED) {
- setTextTo(textView, R.string.language_picker_section_suggested);
+ if (mCountryMode) {
+ setTextTo(textView, R.string.language_picker_regions_section_suggested);
+ } else {
+ setTextTo(textView, R.string.language_picker_section_suggested);
+ }
} else {
if (mCountryMode) {
setTextTo(textView, R.string.region_picker_section_all);
diff --git a/core/java/com/android/internal/inputmethod/IRemoteInputConnection.aidl b/core/java/com/android/internal/inputmethod/IRemoteInputConnection.aidl
index a7dd6f1..7a219c6 100644
--- a/core/java/com/android/internal/inputmethod/IRemoteInputConnection.aidl
+++ b/core/java/com/android/internal/inputmethod/IRemoteInputConnection.aidl
@@ -17,11 +17,15 @@
package com.android.internal.inputmethod;
import android.os.Bundle;
+import android.os.ResultReceiver;
import android.view.KeyEvent;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.DeleteGesture;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputContentInfo;
+import android.view.inputmethod.InsertGesture;
+import android.view.inputmethod.SelectGesture;
import android.view.inputmethod.TextAttribute;
import com.android.internal.infra.AndroidFuture;
@@ -86,6 +90,15 @@
void performPrivateCommand(in InputConnectionCommandHeader header, String action,
in Bundle data);
+ void performHandwritingSelectGesture(in InputConnectionCommandHeader header,
+ in SelectGesture gesture, in ResultReceiver resultReceiver);
+
+ void performHandwritingInsertGesture(in InputConnectionCommandHeader header,
+ in InsertGesture gesture, in ResultReceiver resultReceiver);
+
+ void performHandwritingDeleteGesture(in InputConnectionCommandHeader header,
+ in DeleteGesture gesture, in ResultReceiver resultReceiver);
+
void setComposingRegion(in InputConnectionCommandHeader header, int start, int end);
void setComposingRegionWithTextAttribute(in InputConnectionCommandHeader header, int start,
diff --git a/core/java/com/android/internal/inputmethod/RemoteInputConnectionImpl.java b/core/java/com/android/internal/inputmethod/RemoteInputConnectionImpl.java
index b63ce1b..c65a69f 100644
--- a/core/java/com/android/internal/inputmethod/RemoteInputConnectionImpl.java
+++ b/core/java/com/android/internal/inputmethod/RemoteInputConnectionImpl.java
@@ -31,6 +31,7 @@
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
+import android.os.ResultReceiver;
import android.os.Trace;
import android.util.Log;
import android.util.proto.ProtoOutputStream;
@@ -39,11 +40,15 @@
import android.view.ViewRootImpl;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.DeleteGesture;
import android.view.inputmethod.DumpableInputConnection;
import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.HandwritingGesture;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputContentInfo;
import android.view.inputmethod.InputMethodManager;
+import android.view.inputmethod.InsertGesture;
+import android.view.inputmethod.SelectGesture;
import android.view.inputmethod.TextAttribute;
import android.view.inputmethod.TextSnapshot;
@@ -970,6 +975,67 @@
@Dispatching(cancellable = true)
@Override
+ public void performHandwritingSelectGesture(
+ InputConnectionCommandHeader header, SelectGesture gesture,
+ ResultReceiver resultReceiver) {
+ performHandwritingGestureInternal(header, gesture, resultReceiver);
+ }
+
+ @Dispatching(cancellable = true)
+ @Override
+ public void performHandwritingInsertGesture(
+ InputConnectionCommandHeader header, InsertGesture gesture,
+ ResultReceiver resultReceiver) {
+ performHandwritingGestureInternal(header, gesture, resultReceiver);
+ }
+
+ @Dispatching(cancellable = true)
+ @Override
+ public void performHandwritingDeleteGesture(
+ InputConnectionCommandHeader header, DeleteGesture gesture,
+ ResultReceiver resultReceiver) {
+ performHandwritingGestureInternal(header, gesture, resultReceiver);
+ }
+
+ private <T extends HandwritingGesture> void performHandwritingGestureInternal(
+ InputConnectionCommandHeader header, T gesture, ResultReceiver resultReceiver) {
+ dispatchWithTracing("performHandwritingGesture", () -> {
+ if (header.mSessionId != mCurrentSessionId.get()) {
+ return; // cancelled
+ }
+ InputConnection ic = getInputConnection();
+ if (ic == null || !isActive()) {
+ Log.w(TAG, "performHandwritingGesture on inactive InputConnection");
+ return;
+ }
+ // TODO(b/210039666): implement resultReceiver
+ ic.performHandwritingGesture(gesture, null, null);
+ });
+ }
+
+ /**
+ * Dispatches {@link InputConnection#requestCursorUpdates(int)}.
+ *
+ * <p>This method is intended to be called only from {@link InputMethodManager}.</p>
+ * @param cursorUpdateMode the mode for {@link InputConnection#requestCursorUpdates(int, int)}
+ * @param cursorUpdateFilter the filter for
+ * {@link InputConnection#requestCursorUpdates(int, int)}
+ * @param imeDisplayId displayId on which IME is displayed.
+ */
+ @Dispatching(cancellable = true)
+ public void requestCursorUpdatesFromImm(int cursorUpdateMode, int cursorUpdateFilter,
+ int imeDisplayId) {
+ final int currentSessionId = mCurrentSessionId.get();
+ dispatchWithTracing("requestCursorUpdatesFromImm", () -> {
+ if (currentSessionId != mCurrentSessionId.get()) {
+ return; // cancelled
+ }
+ requestCursorUpdatesInternal(cursorUpdateMode, cursorUpdateFilter, imeDisplayId);
+ });
+ }
+
+ @Dispatching(cancellable = true)
+ @Override
public void requestCursorUpdates(InputConnectionCommandHeader header, int cursorUpdateMode,
int imeDisplayId, AndroidFuture future /* T=Boolean */) {
dispatchWithTracing("requestCursorUpdates", future, () -> {
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index 72de78c..fc4e041 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -87,6 +87,7 @@
import android.annotation.NonNull;
import android.annotation.UiThread;
import android.annotation.WorkerThread;
+import android.app.ActivityThread;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
@@ -292,7 +293,10 @@
UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_CLEAR_ALL,
};
- private static volatile InteractionJankMonitor sInstance;
+ private static class InstanceHolder {
+ public static final InteractionJankMonitor INSTANCE =
+ new InteractionJankMonitor(new HandlerThread(DEFAULT_WORKER_NAME));
+ }
private final DeviceConfig.OnPropertiesChangedListener mPropertiesChangedListener =
this::updateProperties;
@@ -384,15 +388,7 @@
* @return instance of InteractionJankMonitor
*/
public static InteractionJankMonitor getInstance() {
- // Use DCL here since this method might be invoked very often.
- if (sInstance == null) {
- synchronized (InteractionJankMonitor.class) {
- if (sInstance == null) {
- sInstance = new InteractionJankMonitor(new HandlerThread(DEFAULT_WORKER_NAME));
- }
- }
- }
- return sInstance;
+ return InstanceHolder.INSTANCE;
}
/**
@@ -402,6 +398,11 @@
*/
@VisibleForTesting
public InteractionJankMonitor(@NonNull HandlerThread worker) {
+ // Check permission early.
+ DeviceConfig.enforceReadPermission(
+ ActivityThread.currentApplication().getApplicationContext(),
+ DeviceConfig.NAMESPACE_INTERACTION_JANK_MONITOR);
+
mRunningTrackers = new SparseArray<>();
mTimeoutActions = new SparseArray<>();
mWorker = worker;
diff --git a/core/java/com/android/internal/widget/floatingtoolbar/LocalFloatingToolbarPopup.java b/core/java/com/android/internal/widget/floatingtoolbar/LocalFloatingToolbarPopup.java
index 95a4e12..bc729f1 100644
--- a/core/java/com/android/internal/widget/floatingtoolbar/LocalFloatingToolbarPopup.java
+++ b/core/java/com/android/internal/widget/floatingtoolbar/LocalFloatingToolbarPopup.java
@@ -1475,7 +1475,6 @@
contentContainer.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
contentContainer.setTag(FloatingToolbar.FLOATING_TOOLBAR_TAG);
- contentContainer.setContentDescription(FloatingToolbar.FLOATING_TOOLBAR_TAG);
contentContainer.setClipToOutline(true);
return contentContainer;
}
diff --git a/core/jni/OWNERS b/core/jni/OWNERS
index 671e634..7f50204 100644
--- a/core/jni/OWNERS
+++ b/core/jni/OWNERS
@@ -49,7 +49,7 @@
per-file *Zygote* = file:/ZYGOTE_OWNERS
per-file core_jni_helpers.* = file:/ZYGOTE_OWNERS
per-file fd_utils.* = file:/ZYGOTE_OWNERS
-per-file Android.bp = file:platform/build/soong:/OWNERS
+per-file Android.bp = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION}
per-file android_animation_* = file:/core/java/android/animation/OWNERS
per-file android_app_admin_* = file:/core/java/android/app/admin/OWNERS
per-file android_hardware_Usb* = file:/services/usb/OWNERS
diff --git a/core/res/res/layout/floating_popup_container.xml b/core/res/res/layout/floating_popup_container.xml
index ca03737..776a35d 100644
--- a/core/res/res/layout/floating_popup_container.xml
+++ b/core/res/res/layout/floating_popup_container.xml
@@ -16,6 +16,7 @@
*/
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/floating_popup_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="0dp"
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index b7da6ae..7a2c866 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5422,8 +5422,10 @@
<!-- Hint text in a search edit box (used to filter long language / country lists) [CHAR LIMIT=25] -->
<string name="search_language_hint">Type language name</string>
- <!-- List section subheader for the language picker, containing a list of suggested languages determined by the default region [CHAR LIMIT=30] -->
+ <!-- List section subheader for the language picker, containing a list of suggested languages [CHAR LIMIT=30] -->
<string name="language_picker_section_suggested">Suggested</string>
+ <!-- "List section subheader for the language picker, containing a list of suggested regions available for that language [CHAR LIMIT=30] -->
+ <string name="language_picker_regions_section_suggested">Suggested</string>
<!-- List section subheader for the language picker, containing a list of suggested languages determined by the default region [CHAR LIMIT=30] -->
<string name="language_picker_section_suggested_bilingual">Suggested languages</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index d60fc20..91f9bef 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3141,6 +3141,7 @@
<java-symbol type="string" name="language_picker_section_all" />
<java-symbol type="string" name="region_picker_section_all" />
<java-symbol type="string" name="language_picker_section_suggested" />
+ <java-symbol type="string" name="language_picker_regions_section_suggested" />
<java-symbol type="string" name="language_picker_section_suggested_bilingual" />
<java-symbol type="string" name="region_picker_section_suggested_bilingual" />
<java-symbol type="string" name="language_selection_title" />
diff --git a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
index 0a5a4d5..993ecf6 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
@@ -221,15 +221,15 @@
@Test
public void testRecyclePauseActivityItemItem() {
- PauseActivityItem emptyItem = PauseActivityItem.obtain(false, false, 0, false);
- PauseActivityItem item = PauseActivityItem.obtain(true, true, 5, true);
+ PauseActivityItem emptyItem = PauseActivityItem.obtain(false, false, 0, false, false);
+ PauseActivityItem item = PauseActivityItem.obtain(true, true, 5, true, true);
assertNotSame(item, emptyItem);
assertFalse(item.equals(emptyItem));
item.recycle();
assertEquals(item, emptyItem);
- PauseActivityItem item2 = PauseActivityItem.obtain(true, false, 5, true);
+ PauseActivityItem item2 = PauseActivityItem.obtain(true, false, 5, true, true);
assertSame(item, item2);
assertFalse(item2.equals(emptyItem));
}
diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
index e9bbdbe..b292d7d 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
@@ -234,7 +234,8 @@
public void testPause() {
// Write to parcel
PauseActivityItem item = PauseActivityItem.obtain(true /* finished */,
- true /* userLeaving */, 135 /* configChanges */, true /* dontReport */);
+ true /* userLeaving */, 135 /* configChanges */, true /* dontReport */,
+ true /* autoEnteringPip */);
writeAndPrepareForReading(item);
// Read from parcel and assert
diff --git a/core/tests/coretests/src/android/widget/FloatingToolbarUtils.java b/core/tests/coretests/src/android/widget/FloatingToolbarUtils.java
index c6f5924..2d3ed95 100644
--- a/core/tests/coretests/src/android/widget/FloatingToolbarUtils.java
+++ b/core/tests/coretests/src/android/widget/FloatingToolbarUtils.java
@@ -16,13 +16,12 @@
package android.widget;
-import static com.android.internal.widget.floatingtoolbar.FloatingToolbar.FLOATING_TOOLBAR_TAG;
-
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import android.content.res.Resources;
import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.Until;
@@ -33,25 +32,27 @@
final class FloatingToolbarUtils {
private final UiDevice mDevice;
+ private static final BySelector TOOLBAR_CONTAINER_SELECTOR =
+ By.res("android", "floating_popup_container");
FloatingToolbarUtils() {
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
}
void waitForFloatingToolbarPopup() {
- mDevice.wait(Until.findObject(By.desc(FLOATING_TOOLBAR_TAG)), 500);
+ mDevice.wait(Until.findObject(TOOLBAR_CONTAINER_SELECTOR), 500);
}
void assertFloatingToolbarIsDisplayed() {
waitForFloatingToolbarPopup();
- assertThat(mDevice.hasObject(By.desc(FLOATING_TOOLBAR_TAG))).isTrue();
+ assertThat(mDevice.hasObject(TOOLBAR_CONTAINER_SELECTOR)).isTrue();
}
void assertFloatingToolbarContainsItem(String itemLabel) {
waitForFloatingToolbarPopup();
assertWithMessage("Expected to find item labelled [" + itemLabel + "]")
.that(mDevice.hasObject(
- By.desc(FLOATING_TOOLBAR_TAG).hasDescendant(By.text(itemLabel))))
+ TOOLBAR_CONTAINER_SELECTOR.hasDescendant(By.text(itemLabel))))
.isTrue();
}
@@ -59,14 +60,14 @@
waitForFloatingToolbarPopup();
assertWithMessage("Expected to not find item labelled [" + itemLabel + "]")
.that(mDevice.hasObject(
- By.desc(FLOATING_TOOLBAR_TAG).hasDescendant(By.text(itemLabel))))
+ TOOLBAR_CONTAINER_SELECTOR.hasDescendant(By.text(itemLabel))))
.isFalse();
}
void assertFloatingToolbarContainsItemAtIndex(String itemLabel, int index) {
waitForFloatingToolbarPopup();
assertWithMessage("Expected to find item labelled [" + itemLabel + "] at index " + index)
- .that(mDevice.findObject(By.desc(FLOATING_TOOLBAR_TAG))
+ .that(mDevice.findObject(TOOLBAR_CONTAINER_SELECTOR)
.findObjects(By.clickable(true))
.get(index)
.getChildren()
@@ -77,7 +78,7 @@
void clickFloatingToolbarItem(String label) {
waitForFloatingToolbarPopup();
- mDevice.findObject(By.desc(FLOATING_TOOLBAR_TAG))
+ mDevice.findObject(TOOLBAR_CONTAINER_SELECTOR)
.findObject(By.text(label))
.click();
}
@@ -85,13 +86,13 @@
void clickFloatingToolbarOverflowItem(String label) {
// TODO: There might be a benefit to combining this with "clickFloatingToolbarItem" method.
waitForFloatingToolbarPopup();
- mDevice.findObject(By.desc(FLOATING_TOOLBAR_TAG))
+ mDevice.findObject(TOOLBAR_CONTAINER_SELECTOR)
.findObject(By.desc(str(R.string.floating_toolbar_open_overflow_description)))
.click();
mDevice.wait(
- Until.findObject(By.desc(FLOATING_TOOLBAR_TAG).hasDescendant(By.text(label))),
+ Until.findObject(TOOLBAR_CONTAINER_SELECTOR.hasDescendant(By.text(label))),
1000);
- mDevice.findObject(By.desc(FLOATING_TOOLBAR_TAG))
+ mDevice.findObject(TOOLBAR_CONTAINER_SELECTOR)
.findObject(By.text(label))
.click();
}
diff --git a/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java b/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
index f4a6f02..613eddd 100644
--- a/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
+++ b/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
@@ -298,8 +298,8 @@
private void pauseActivity(ActivityClientRecord r) {
mThread.handlePauseActivity(r, false /* finished */,
- false /* userLeaving */, 0 /* configChanges */, null /* pendingActions */,
- "test");
+ false /* userLeaving */, 0 /* configChanges */, false /* autoEnteringPip */,
+ null /* pendingActions */, "test");
}
private void stopActivity(ActivityClientRecord r) {
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 03d22ac..748032b 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -2065,6 +2065,12 @@
"group": "WM_DEBUG_CONFIGURATION",
"at": "com\/android\/server\/wm\/ActivityRecord.java"
},
+ "-108248992": {
+ "message": "Defer transition ready for TaskFragmentTransaction=%s",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+ "at": "com\/android\/server\/wm\/TaskFragmentOrganizerController.java"
+ },
"-106400104": {
"message": "Preload recents with %s",
"level": "DEBUG",
@@ -2113,6 +2119,12 @@
"group": "WM_DEBUG_STATES",
"at": "com\/android\/server\/wm\/TaskFragment.java"
},
+ "-79016993": {
+ "message": "Continue transition ready for TaskFragmentTransaction=%s",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+ "at": "com\/android\/server\/wm\/TaskFragmentOrganizerController.java"
+ },
"-70719599": {
"message": "Unregister remote animations for organizer=%s uid=%d pid=%d",
"level": "VERBOSE",
diff --git a/data/fonts/fonts.xml b/data/fonts/fonts.xml
index f8c015f..8e2a59c 100644
--- a/data/fonts/fonts.xml
+++ b/data/fonts/fonts.xml
@@ -39,7 +39,11 @@
<axis tag="wdth" stylevalue="100" />
<axis tag="wght" stylevalue="300" />
</font>
- <font weight="400" style="normal">RobotoStatic-Regular.ttf</font>
+ <font weight="400" style="normal">Roboto-Regular.ttf
+ <axis tag="ital" stylevalue="0" />
+ <axis tag="wdth" stylevalue="100" />
+ <axis tag="wght" stylevalue="400" />
+ </font>
<font weight="500" style="normal">Roboto-Regular.ttf
<axis tag="ital" stylevalue="0" />
<axis tag="wdth" stylevalue="100" />
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
new file mode 100644
index 0000000..cc4db93
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
@@ -0,0 +1,234 @@
+/*
+ * 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.wm.shell.activityembedding;
+
+import static android.graphics.Matrix.MSCALE_X;
+import static android.graphics.Matrix.MTRANS_X;
+import static android.graphics.Matrix.MTRANS_Y;
+
+import android.annotation.CallSuper;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.Choreographer;
+import android.view.SurfaceControl;
+import android.view.animation.Animation;
+import android.view.animation.Transformation;
+import android.window.TransitionInfo;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Wrapper to handle the ActivityEmbedding animation update in one
+ * {@link SurfaceControl.Transaction}.
+ */
+class ActivityEmbeddingAnimationAdapter {
+
+ /**
+ * If {@link #mOverrideLayer} is set to this value, we don't want to override the surface layer.
+ */
+ private static final int LAYER_NO_OVERRIDE = -1;
+
+ final Animation mAnimation;
+ final TransitionInfo.Change mChange;
+ final SurfaceControl mLeash;
+
+ final Transformation mTransformation = new Transformation();
+ final float[] mMatrix = new float[9];
+ final float[] mVecs = new float[4];
+ final Rect mRect = new Rect();
+ private boolean mIsFirstFrame = true;
+ private int mOverrideLayer = LAYER_NO_OVERRIDE;
+
+ ActivityEmbeddingAnimationAdapter(@NonNull Animation animation,
+ @NonNull TransitionInfo.Change change) {
+ this(animation, change, change.getLeash());
+ }
+
+ /**
+ * @param leash the surface to animate, which is not necessary the same as
+ * {@link TransitionInfo.Change#getLeash()}, it can be a screenshot for example.
+ */
+ ActivityEmbeddingAnimationAdapter(@NonNull Animation animation,
+ @NonNull TransitionInfo.Change change, @NonNull SurfaceControl leash) {
+ mAnimation = animation;
+ mChange = change;
+ mLeash = leash;
+ }
+
+ /**
+ * Surface layer to be set at the first frame of the animation. We will not set the layer if it
+ * is set to {@link #LAYER_NO_OVERRIDE}.
+ */
+ final void overrideLayer(int layer) {
+ mOverrideLayer = layer;
+ }
+
+ /** Called on frame update. */
+ final void onAnimationUpdate(@NonNull SurfaceControl.Transaction t, long currentPlayTime) {
+ if (mIsFirstFrame) {
+ t.show(mLeash);
+ if (mOverrideLayer != LAYER_NO_OVERRIDE) {
+ t.setLayer(mLeash, mOverrideLayer);
+ }
+ mIsFirstFrame = false;
+ }
+
+ // Extract the transformation to the current time.
+ mAnimation.getTransformation(Math.min(currentPlayTime, mAnimation.getDuration()),
+ mTransformation);
+ t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
+ onAnimationUpdateInner(t);
+ }
+
+ /** To be overridden by subclasses to adjust the animation surface change. */
+ void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
+ final Point offset = mChange.getEndRelOffset();
+ mTransformation.getMatrix().postTranslate(offset.x, offset.y);
+ t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
+ t.setAlpha(mLeash, mTransformation.getAlpha());
+ // Get current animation position.
+ final int positionX = Math.round(mMatrix[MTRANS_X]);
+ final int positionY = Math.round(mMatrix[MTRANS_Y]);
+ // The exiting surface starts at position: Change#getEndRelOffset() and moves with
+ // positionX varying. Offset our crop region by the amount we have slided so crop
+ // regions stays exactly on the original container in split.
+ final int cropOffsetX = offset.x - positionX;
+ final int cropOffsetY = offset.y - positionY;
+ final Rect cropRect = new Rect();
+ cropRect.set(mChange.getEndAbsBounds());
+ // Because window crop uses absolute position.
+ cropRect.offsetTo(0, 0);
+ cropRect.offset(cropOffsetX, cropOffsetY);
+ t.setCrop(mLeash, cropRect);
+ }
+
+ /** Called after animation finished. */
+ @CallSuper
+ void onAnimationEnd(@NonNull SurfaceControl.Transaction t) {
+ onAnimationUpdate(t, mAnimation.getDuration());
+ }
+
+ final long getDurationHint() {
+ return mAnimation.computeDurationHint();
+ }
+
+ /**
+ * Should be used when the {@link TransitionInfo.Change} is in split with others, and wants to
+ * animate together as one. This adapter will offset the animation leash to make the animate of
+ * two windows look like a single window.
+ */
+ static class SplitAdapter extends ActivityEmbeddingAnimationAdapter {
+ private final boolean mIsLeftHalf;
+ private final int mWholeAnimationWidth;
+
+ /**
+ * @param isLeftHalf whether this is the left half of the animation.
+ * @param wholeAnimationWidth the whole animation windows width.
+ */
+ SplitAdapter(@NonNull Animation animation, @NonNull TransitionInfo.Change change,
+ boolean isLeftHalf, int wholeAnimationWidth) {
+ super(animation, change);
+ mIsLeftHalf = isLeftHalf;
+ mWholeAnimationWidth = wholeAnimationWidth;
+ if (wholeAnimationWidth == 0) {
+ throw new IllegalArgumentException("SplitAdapter must provide wholeAnimationWidth");
+ }
+ }
+
+ @Override
+ void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
+ final Point offset = mChange.getEndRelOffset();
+ float posX = offset.x;
+ final float posY = offset.y;
+ // This window is half of the whole animation window. Offset left/right to make it
+ // look as one with the other half.
+ mTransformation.getMatrix().getValues(mMatrix);
+ final int changeWidth = mChange.getEndAbsBounds().width();
+ final float scaleX = mMatrix[MSCALE_X];
+ final float totalOffset = mWholeAnimationWidth * (1 - scaleX) / 2;
+ final float curOffset = changeWidth * (1 - scaleX) / 2;
+ final float offsetDiff = totalOffset - curOffset;
+ if (mIsLeftHalf) {
+ posX += offsetDiff;
+ } else {
+ posX -= offsetDiff;
+ }
+ mTransformation.getMatrix().postTranslate(posX, posY);
+ t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
+ t.setAlpha(mLeash, mTransformation.getAlpha());
+ }
+ }
+
+ /**
+ * Should be used for the animation of the snapshot of a {@link TransitionInfo.Change} that has
+ * size change.
+ */
+ static class SnapshotAdapter extends ActivityEmbeddingAnimationAdapter {
+
+ SnapshotAdapter(@NonNull Animation animation, @NonNull TransitionInfo.Change change,
+ @NonNull SurfaceControl snapshotLeash) {
+ super(animation, change, snapshotLeash);
+ }
+
+ @Override
+ void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
+ // Snapshot should always be placed at the top left of the animation leash.
+ mTransformation.getMatrix().postTranslate(0, 0);
+ t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
+ t.setAlpha(mLeash, mTransformation.getAlpha());
+ }
+
+ @Override
+ void onAnimationEnd(@NonNull SurfaceControl.Transaction t) {
+ super.onAnimationEnd(t);
+ // Remove the screenshot leash after animation is finished.
+ t.remove(mLeash);
+ }
+ }
+
+ /**
+ * Should be used for the animation of the {@link TransitionInfo.Change} that has size change.
+ */
+ static class BoundsChangeAdapter extends ActivityEmbeddingAnimationAdapter {
+
+ BoundsChangeAdapter(@NonNull Animation animation, @NonNull TransitionInfo.Change change) {
+ super(animation, change);
+ }
+
+ @Override
+ void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
+ final Point offset = mChange.getEndRelOffset();
+ mTransformation.getMatrix().postTranslate(offset.x, offset.y);
+ t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
+ t.setAlpha(mLeash, mTransformation.getAlpha());
+
+ // The following applies an inverse scale to the clip-rect so that it crops "after" the
+ // scale instead of before.
+ mVecs[1] = mVecs[2] = 0;
+ mVecs[0] = mVecs[3] = 1;
+ mTransformation.getMatrix().mapVectors(mVecs);
+ mVecs[0] = 1.f / mVecs[0];
+ mVecs[3] = 1.f / mVecs[3];
+ final Rect clipRect = mTransformation.getClipRect();
+ mRect.left = (int) (clipRect.left * mVecs[0] + 0.5f);
+ mRect.right = (int) (clipRect.right * mVecs[0] + 0.5f);
+ mRect.top = (int) (clipRect.top * mVecs[3] + 0.5f);
+ mRect.bottom = (int) (clipRect.bottom * mVecs[3] + 0.5f);
+ t.setCrop(mLeash, mRect);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
new file mode 100644
index 0000000..7e0795d
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
@@ -0,0 +1,290 @@
+/*
+ * 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.wm.shell.activityembedding;
+
+import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.util.Log;
+import android.view.SurfaceControl;
+import android.view.animation.Animation;
+import android.window.TransitionInfo;
+import android.window.WindowContainerToken;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.wm.shell.common.ScreenshotUtils;
+import com.android.wm.shell.transition.Transitions;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.BiFunction;
+
+/** To run the ActivityEmbedding animations. */
+class ActivityEmbeddingAnimationRunner {
+
+ private static final String TAG = "ActivityEmbeddingAnimR";
+
+ private final ActivityEmbeddingController mController;
+ @VisibleForTesting
+ final ActivityEmbeddingAnimationSpec mAnimationSpec;
+
+ ActivityEmbeddingAnimationRunner(@NonNull Context context,
+ @NonNull ActivityEmbeddingController controller) {
+ mController = controller;
+ mAnimationSpec = new ActivityEmbeddingAnimationSpec(context);
+ }
+
+ /** Creates and starts animation for ActivityEmbedding transition. */
+ void startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction) {
+ final Animator animator = createAnimator(info, startTransaction, finishTransaction,
+ () -> mController.onAnimationFinished(transition));
+ startTransaction.apply();
+ animator.start();
+ }
+
+ /**
+ * Sets transition animation scale settings value.
+ * @param scale The setting value of transition animation scale.
+ */
+ void setAnimScaleSetting(float scale) {
+ mAnimationSpec.setAnimScaleSetting(scale);
+ }
+
+ /** Creates the animator for the given {@link TransitionInfo}. */
+ @VisibleForTesting
+ @NonNull
+ Animator createAnimator(@NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Runnable animationFinishCallback) {
+ final List<ActivityEmbeddingAnimationAdapter> adapters =
+ createAnimationAdapters(info, startTransaction);
+ long duration = 0;
+ for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
+ duration = Math.max(duration, adapter.getDurationHint());
+ }
+ final ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
+ animator.setDuration(duration);
+ animator.addUpdateListener((anim) -> {
+ // Update all adapters in the same transaction.
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
+ adapter.onAnimationUpdate(t, animator.getCurrentPlayTime());
+ }
+ t.apply();
+ });
+ animator.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {}
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
+ adapter.onAnimationEnd(t);
+ }
+ t.apply();
+ animationFinishCallback.run();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {}
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {}
+ });
+ return animator;
+ }
+
+ /**
+ * Creates list of {@link ActivityEmbeddingAnimationAdapter} to handle animations on all window
+ * changes.
+ */
+ @NonNull
+ private List<ActivityEmbeddingAnimationAdapter> createAnimationAdapters(
+ @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction) {
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if (change.getMode() == TRANSIT_CHANGE
+ && !change.getStartAbsBounds().equals(change.getEndAbsBounds())) {
+ return createChangeAnimationAdapters(info, startTransaction);
+ }
+ }
+ if (Transitions.isClosingType(info.getType())) {
+ return createCloseAnimationAdapters(info);
+ }
+ return createOpenAnimationAdapters(info);
+ }
+
+ @NonNull
+ private List<ActivityEmbeddingAnimationAdapter> createOpenAnimationAdapters(
+ @NonNull TransitionInfo info) {
+ return createOpenCloseAnimationAdapters(info, true /* isOpening */,
+ mAnimationSpec::loadOpenAnimation);
+ }
+
+ @NonNull
+ private List<ActivityEmbeddingAnimationAdapter> createCloseAnimationAdapters(
+ @NonNull TransitionInfo info) {
+ return createOpenCloseAnimationAdapters(info, false /* isOpening */,
+ mAnimationSpec::loadCloseAnimation);
+ }
+
+ /**
+ * Creates {@link ActivityEmbeddingAnimationAdapter} for OPEN and CLOSE types of transition.
+ * @param isOpening {@code true} for OPEN type, {@code false} for CLOSE type.
+ */
+ @NonNull
+ private List<ActivityEmbeddingAnimationAdapter> createOpenCloseAnimationAdapters(
+ @NonNull TransitionInfo info, boolean isOpening,
+ @NonNull BiFunction<TransitionInfo.Change, Rect, Animation> animationProvider) {
+ // We need to know if the change window is only a partial of the whole animation screen.
+ // If so, we will need to adjust it to make the whole animation screen looks like one.
+ final List<TransitionInfo.Change> openingChanges = new ArrayList<>();
+ final List<TransitionInfo.Change> closingChanges = new ArrayList<>();
+ final Rect openingWholeScreenBounds = new Rect();
+ final Rect closingWholeScreenBounds = new Rect();
+ for (TransitionInfo.Change change : info.getChanges()) {
+ final Rect bounds = new Rect(change.getEndAbsBounds());
+ final Point offset = change.getEndRelOffset();
+ bounds.offsetTo(offset.x, offset.y);
+ if (Transitions.isOpeningType(change.getMode())) {
+ openingChanges.add(change);
+ openingWholeScreenBounds.union(bounds);
+ } else {
+ closingChanges.add(change);
+ closingWholeScreenBounds.union(bounds);
+ }
+ }
+
+ // For OPEN transition, open windows should be above close windows.
+ // For CLOSE transition, open windows should be below close windows.
+ int offsetLayer = TYPE_LAYER_OFFSET;
+ final List<ActivityEmbeddingAnimationAdapter> adapters = new ArrayList<>();
+ for (TransitionInfo.Change change : openingChanges) {
+ final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter(
+ change, animationProvider, openingWholeScreenBounds);
+ if (isOpening) {
+ adapter.overrideLayer(offsetLayer++);
+ }
+ adapters.add(adapter);
+ }
+ for (TransitionInfo.Change change : closingChanges) {
+ final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter(
+ change, animationProvider, closingWholeScreenBounds);
+ if (!isOpening) {
+ adapter.overrideLayer(offsetLayer++);
+ }
+ adapters.add(adapter);
+ }
+ return adapters;
+ }
+
+ @NonNull
+ private ActivityEmbeddingAnimationAdapter createOpenCloseAnimationAdapter(
+ @NonNull TransitionInfo.Change change,
+ @NonNull BiFunction<TransitionInfo.Change, Rect, Animation> animationProvider,
+ @NonNull Rect wholeAnimationBounds) {
+ final Animation animation = animationProvider.apply(change, wholeAnimationBounds);
+ final Rect bounds = new Rect(change.getEndAbsBounds());
+ final Point offset = change.getEndRelOffset();
+ bounds.offsetTo(offset.x, offset.y);
+ if (bounds.left == wholeAnimationBounds.left
+ && bounds.right != wholeAnimationBounds.right) {
+ // This is the left split of the whole animation window.
+ return new ActivityEmbeddingAnimationAdapter.SplitAdapter(animation, change,
+ true /* isLeftHalf */, wholeAnimationBounds.width());
+ } else if (bounds.left != wholeAnimationBounds.left
+ && bounds.right == wholeAnimationBounds.right) {
+ // This is the right split of the whole animation window.
+ return new ActivityEmbeddingAnimationAdapter.SplitAdapter(animation, change,
+ false /* isLeftHalf */, wholeAnimationBounds.width());
+ }
+ // Open/close window that fills the whole animation.
+ return new ActivityEmbeddingAnimationAdapter(animation, change);
+ }
+
+ @NonNull
+ private List<ActivityEmbeddingAnimationAdapter> createChangeAnimationAdapters(
+ @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction) {
+ final List<ActivityEmbeddingAnimationAdapter> adapters = new ArrayList<>();
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if (change.getMode() == TRANSIT_CHANGE
+ && !change.getStartAbsBounds().equals(change.getEndAbsBounds())) {
+ // This is the window with bounds change.
+ final WindowContainerToken parentToken = change.getParent();
+ final Rect parentBounds;
+ if (parentToken != null) {
+ TransitionInfo.Change parentChange = info.getChange(parentToken);
+ parentBounds = parentChange != null
+ ? parentChange.getEndAbsBounds()
+ : change.getEndAbsBounds();
+ } else {
+ parentBounds = change.getEndAbsBounds();
+ }
+ final Animation[] animations =
+ mAnimationSpec.createChangeBoundsChangeAnimations(change, parentBounds);
+ // Adapter for the starting screenshot leash.
+ final SurfaceControl screenshotLeash = createScreenshot(change, startTransaction);
+ if (screenshotLeash != null) {
+ // The screenshot leash will be removed in SnapshotAdapter#onAnimationEnd
+ adapters.add(new ActivityEmbeddingAnimationAdapter.SnapshotAdapter(
+ animations[0], change, screenshotLeash));
+ } else {
+ Log.e(TAG, "Failed to take screenshot for change=" + change);
+ }
+ // Adapter for the ending bounds changed leash.
+ adapters.add(new ActivityEmbeddingAnimationAdapter.BoundsChangeAdapter(
+ animations[1], change));
+ continue;
+ }
+
+ // These are the other windows that don't have bounds change in the same transition.
+ final Animation animation;
+ if (!TransitionInfo.isIndependent(change, info)) {
+ // No-op if it will be covered by the changing parent window.
+ animation = ActivityEmbeddingAnimationSpec.createNoopAnimation(change);
+ } else if (Transitions.isClosingType(change.getMode())) {
+ animation = mAnimationSpec.createChangeBoundsCloseAnimation(change);
+ } else {
+ animation = mAnimationSpec.createChangeBoundsOpenAnimation(change);
+ }
+ adapters.add(new ActivityEmbeddingAnimationAdapter(animation, change));
+ }
+ return adapters;
+ }
+
+ /** Takes a screenshot of the given {@link TransitionInfo.Change} surface. */
+ @Nullable
+ private SurfaceControl createScreenshot(@NonNull TransitionInfo.Change change,
+ @NonNull SurfaceControl.Transaction startTransaction) {
+ final Rect cropBounds = new Rect(change.getStartAbsBounds());
+ cropBounds.offsetTo(0, 0);
+ return ScreenshotUtils.takeScreenshot(startTransaction, change.getLeash(), cropBounds,
+ Integer.MAX_VALUE);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
new file mode 100644
index 0000000..6f06f28
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
@@ -0,0 +1,212 @@
+/*
+ * 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.wm.shell.activityembedding;
+
+
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.AnimationUtils;
+import android.view.animation.ClipRectAnimation;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.ScaleAnimation;
+import android.view.animation.TranslateAnimation;
+import android.window.TransitionInfo;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.R;
+import com.android.internal.policy.TransitionAnimation;
+import com.android.wm.shell.transition.Transitions;
+
+/** Animation spec for ActivityEmbedding transition. */
+// TODO(b/206557124): provide an easier way to customize animation
+class ActivityEmbeddingAnimationSpec {
+
+ private static final String TAG = "ActivityEmbeddingAnimSpec";
+ private static final int CHANGE_ANIMATION_DURATION = 517;
+ private static final int CHANGE_ANIMATION_FADE_DURATION = 80;
+ private static final int CHANGE_ANIMATION_FADE_OFFSET = 30;
+
+ private final Context mContext;
+ private final TransitionAnimation mTransitionAnimation;
+ private final Interpolator mFastOutExtraSlowInInterpolator;
+ private final LinearInterpolator mLinearInterpolator;
+ private float mTransitionAnimationScaleSetting;
+
+ ActivityEmbeddingAnimationSpec(@NonNull Context context) {
+ mContext = context;
+ mTransitionAnimation = new TransitionAnimation(mContext, false /* debug */, TAG);
+ mFastOutExtraSlowInInterpolator = AnimationUtils.loadInterpolator(
+ mContext, android.R.interpolator.fast_out_extra_slow_in);
+ mLinearInterpolator = new LinearInterpolator();
+ }
+
+ /**
+ * Sets transition animation scale settings value.
+ * @param scale The setting value of transition animation scale.
+ */
+ void setAnimScaleSetting(float scale) {
+ mTransitionAnimationScaleSetting = scale;
+ }
+
+ /** For window that doesn't need to be animated. */
+ @NonNull
+ static Animation createNoopAnimation(@NonNull TransitionInfo.Change change) {
+ // Noop but just keep the window showing/hiding.
+ final float alpha = Transitions.isClosingType(change.getMode()) ? 0f : 1f;
+ return new AlphaAnimation(alpha, alpha);
+ }
+
+ /** Animation for window that is opening in a change transition. */
+ @NonNull
+ Animation createChangeBoundsOpenAnimation(@NonNull TransitionInfo.Change change) {
+ final Rect bounds = change.getEndAbsBounds();
+ final Point offset = change.getEndRelOffset();
+ // The window will be animated in from left or right depends on its position.
+ final int startLeft = offset.x == 0 ? -bounds.width() : bounds.width();
+
+ // The position should be 0-based as we will post translate in
+ // ActivityEmbeddingAnimationAdapter#onAnimationUpdate
+ final Animation animation = new TranslateAnimation(startLeft, 0, 0, 0);
+ animation.setInterpolator(mFastOutExtraSlowInInterpolator);
+ animation.setDuration(CHANGE_ANIMATION_DURATION);
+ animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height());
+ animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+ return animation;
+ }
+
+ /** Animation for window that is closing in a change transition. */
+ @NonNull
+ Animation createChangeBoundsCloseAnimation(@NonNull TransitionInfo.Change change) {
+ final Rect bounds = change.getEndAbsBounds();
+ final Point offset = change.getEndRelOffset();
+ // The window will be animated out to left or right depends on its position.
+ final int endLeft = offset.x == 0 ? -bounds.width() : bounds.width();
+
+ // The position should be 0-based as we will post translate in
+ // ActivityEmbeddingAnimationAdapter#onAnimationUpdate
+ final Animation animation = new TranslateAnimation(0, endLeft, 0, 0);
+ animation.setInterpolator(mFastOutExtraSlowInInterpolator);
+ animation.setDuration(CHANGE_ANIMATION_DURATION);
+ animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height());
+ animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+ return animation;
+ }
+
+ /**
+ * Animation for window that is changing (bounds change) in a change transition.
+ * @return the return array always has two elements. The first one is for the start leash, and
+ * the second one is for the end leash.
+ */
+ @NonNull
+ Animation[] createChangeBoundsChangeAnimations(@NonNull TransitionInfo.Change change,
+ @NonNull Rect parentBounds) {
+ // Both start bounds and end bounds are in screen coordinates. We will post translate
+ // to the local coordinates in ActivityEmbeddingAnimationAdapter#onAnimationUpdate
+ final Rect startBounds = change.getStartAbsBounds();
+ final Rect endBounds = change.getEndAbsBounds();
+ float scaleX = ((float) startBounds.width()) / endBounds.width();
+ float scaleY = ((float) startBounds.height()) / endBounds.height();
+ // Start leash is a child of the end leash. Reverse the scale so that the start leash won't
+ // be scaled up with its parent.
+ float startScaleX = 1.f / scaleX;
+ float startScaleY = 1.f / scaleY;
+
+ // The start leash will be fade out.
+ final AnimationSet startSet = new AnimationSet(false /* shareInterpolator */);
+ final Animation startAlpha = new AlphaAnimation(1f, 0f);
+ startAlpha.setInterpolator(mLinearInterpolator);
+ startAlpha.setDuration(CHANGE_ANIMATION_FADE_DURATION);
+ startAlpha.setStartOffset(CHANGE_ANIMATION_FADE_OFFSET);
+ startSet.addAnimation(startAlpha);
+ final Animation startScale = new ScaleAnimation(startScaleX, startScaleX, startScaleY,
+ startScaleY);
+ startScale.setInterpolator(mFastOutExtraSlowInInterpolator);
+ startScale.setDuration(CHANGE_ANIMATION_DURATION);
+ startSet.addAnimation(startScale);
+ startSet.initialize(startBounds.width(), startBounds.height(), endBounds.width(),
+ endBounds.height());
+ startSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+
+ // The end leash will be moved into the end position while scaling.
+ final AnimationSet endSet = new AnimationSet(true /* shareInterpolator */);
+ endSet.setInterpolator(mFastOutExtraSlowInInterpolator);
+ final Animation endScale = new ScaleAnimation(scaleX, 1, scaleY, 1);
+ endScale.setDuration(CHANGE_ANIMATION_DURATION);
+ endSet.addAnimation(endScale);
+ // The position should be 0-based as we will post translate in
+ // ActivityEmbeddingAnimationAdapter#onAnimationUpdate
+ final Animation endTranslate = new TranslateAnimation(startBounds.left - endBounds.left, 0,
+ 0, 0);
+ endTranslate.setDuration(CHANGE_ANIMATION_DURATION);
+ endSet.addAnimation(endTranslate);
+ // The end leash is resizing, we should update the window crop based on the clip rect.
+ final Rect startClip = new Rect(startBounds);
+ final Rect endClip = new Rect(endBounds);
+ startClip.offsetTo(0, 0);
+ endClip.offsetTo(0, 0);
+ final Animation clipAnim = new ClipRectAnimation(startClip, endClip);
+ clipAnim.setDuration(CHANGE_ANIMATION_DURATION);
+ endSet.addAnimation(clipAnim);
+ endSet.initialize(startBounds.width(), startBounds.height(), parentBounds.width(),
+ parentBounds.height());
+ endSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+
+ return new Animation[]{startSet, endSet};
+ }
+
+ @NonNull
+ Animation loadOpenAnimation(@NonNull TransitionInfo.Change change,
+ @NonNull Rect wholeAnimationBounds) {
+ final boolean isEnter = Transitions.isOpeningType(change.getMode());
+ final Animation animation;
+ // TODO(b/207070762):
+ // 1. Implement clearTop version: R.anim.task_fragment_clear_top_close_enter/exit
+ // 2. Implement edgeExtension version
+ animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
+ ? R.anim.task_fragment_open_enter
+ : R.anim.task_fragment_open_exit);
+ final Rect bounds = change.getEndAbsBounds();
+ animation.initialize(bounds.width(), bounds.height(),
+ wholeAnimationBounds.width(), wholeAnimationBounds.height());
+ animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+ return animation;
+ }
+
+ @NonNull
+ Animation loadCloseAnimation(@NonNull TransitionInfo.Change change,
+ @NonNull Rect wholeAnimationBounds) {
+ final boolean isEnter = Transitions.isOpeningType(change.getMode());
+ final Animation animation;
+ // TODO(b/207070762):
+ // 1. Implement clearTop version: R.anim.task_fragment_clear_top_close_enter/exit
+ // 2. Implement edgeExtension version
+ animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
+ ? R.anim.task_fragment_close_enter
+ : R.anim.task_fragment_close_exit);
+ final Rect bounds = change.getEndAbsBounds();
+ animation.initialize(bounds.width(), bounds.height(),
+ wholeAnimationBounds.width(), wholeAnimationBounds.height());
+ animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+ return animation;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
index b305897..e0004fc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
@@ -18,8 +18,11 @@
import static android.window.TransitionInfo.FLAG_IS_EMBEDDED;
+import static java.util.Objects.requireNonNull;
+
import android.content.Context;
import android.os.IBinder;
+import android.util.ArrayMap;
import android.view.SurfaceControl;
import android.window.TransitionInfo;
import android.window.TransitionRequestInfo;
@@ -28,6 +31,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
@@ -37,15 +41,37 @@
public class ActivityEmbeddingController implements Transitions.TransitionHandler {
private final Context mContext;
- private final Transitions mTransitions;
+ @VisibleForTesting
+ final Transitions mTransitions;
+ @VisibleForTesting
+ final ActivityEmbeddingAnimationRunner mAnimationRunner;
- public ActivityEmbeddingController(Context context, ShellInit shellInit,
- Transitions transitions) {
- mContext = context;
- mTransitions = transitions;
- if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- shellInit.addInitCallback(this::onInit, this);
- }
+ /**
+ * Keeps track of the currently-running transition callback associated with each transition
+ * token.
+ */
+ private final ArrayMap<IBinder, Transitions.TransitionFinishCallback> mTransitionCallbacks =
+ new ArrayMap<>();
+
+ private ActivityEmbeddingController(@NonNull Context context, @NonNull ShellInit shellInit,
+ @NonNull Transitions transitions) {
+ mContext = requireNonNull(context);
+ mTransitions = requireNonNull(transitions);
+ mAnimationRunner = new ActivityEmbeddingAnimationRunner(context, this);
+
+ shellInit.addInitCallback(this::onInit, this);
+ }
+
+ /**
+ * Creates {@link ActivityEmbeddingController}, returns {@code null} if the feature is not
+ * supported.
+ */
+ @Nullable
+ public static ActivityEmbeddingController create(@NonNull Context context,
+ @NonNull ShellInit shellInit, @NonNull Transitions transitions) {
+ return Transitions.ENABLE_SHELL_TRANSITIONS
+ ? new ActivityEmbeddingController(context, shellInit, transitions)
+ : null;
}
/** Registers to handle transitions. */
@@ -66,9 +92,9 @@
}
}
- // TODO(b/207070762) Implement AE animation.
- startTransaction.apply();
- finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
+ // Start ActivityEmbedding animation.
+ mTransitionCallbacks.put(transition, finishCallback);
+ mAnimationRunner.startAnimation(transition, info, startTransaction, finishTransaction);
return true;
}
@@ -79,6 +105,21 @@
return null;
}
+ @Override
+ public void setAnimScaleSetting(float scale) {
+ mAnimationRunner.setAnimScaleSetting(scale);
+ }
+
+ /** Called when the animation is finished. */
+ void onAnimationFinished(@NonNull IBinder transition) {
+ final Transitions.TransitionFinishCallback callback =
+ mTransitionCallbacks.remove(transition);
+ if (callback == null) {
+ throw new IllegalStateException("No finish callback found");
+ }
+ callback.onTransitionFinished(null /* wct */, null /* wctCB */);
+ }
+
private static boolean isEmbedded(@NonNull TransitionInfo.Change change) {
return (change.getFlags() & FLAG_IS_EMBEDDED) != 0;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index 2c02006..99b8885 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -821,7 +821,7 @@
/**
* Description of current bubble state.
*/
- public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
+ public void dump(@NonNull PrintWriter pw) {
pw.print("key: "); pw.println(mKey);
pw.print(" showInShade: "); pw.println(showInShade());
pw.print(" showDot: "); pw.println(showDot());
@@ -831,7 +831,7 @@
pw.print(" suppressNotif: "); pw.println(shouldSuppressNotification());
pw.print(" autoExpand: "); pw.println(shouldAutoExpand());
if (mExpandedView != null) {
- mExpandedView.dump(pw, args);
+ mExpandedView.dump(pw);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index de26b549..dcbb272 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -72,7 +72,6 @@
import android.service.notification.NotificationListenerService.RankingMap;
import android.util.Log;
import android.util.Pair;
-import android.util.Slog;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
@@ -100,6 +99,7 @@
import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
import com.android.wm.shell.pip.PinnedStackListenerForwarder;
import com.android.wm.shell.sysui.ConfigurationChangeListener;
+import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
@@ -159,6 +159,7 @@
private final TaskViewTransitions mTaskViewTransitions;
private final SyncTransactionQueue mSyncQueue;
private final ShellController mShellController;
+ private final ShellCommandHandler mShellCommandHandler;
// Used to post to main UI thread
private final ShellExecutor mMainExecutor;
@@ -229,6 +230,7 @@
public BubbleController(Context context,
ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
ShellController shellController,
BubbleData data,
@Nullable BubbleStackView.SurfaceSynchronizer synchronizer,
@@ -252,6 +254,7 @@
TaskViewTransitions taskViewTransitions,
SyncTransactionQueue syncQueue) {
mContext = context;
+ mShellCommandHandler = shellCommandHandler;
mShellController = shellController;
mLauncherApps = launcherApps;
mBarService = statusBarService == null
@@ -431,6 +434,7 @@
mCurrentProfiles = userProfiles;
mShellController.addConfigurationChangeListener(this);
+ mShellCommandHandler.addDumpCallback(this::dump, this);
}
@VisibleForTesting
@@ -538,7 +542,6 @@
if (mNotifEntryToExpandOnShadeUnlock != null) {
expandStackAndSelectBubble(mNotifEntryToExpandOnShadeUnlock);
- mNotifEntryToExpandOnShadeUnlock = null;
}
updateStack();
@@ -925,15 +928,6 @@
return (isSummary && isSuppressedSummary) || isSuppressedBubble;
}
- private void removeSuppressedSummaryIfNecessary(String groupKey, Consumer<String> callback) {
- if (mBubbleData.isSummarySuppressed(groupKey)) {
- mBubbleData.removeSuppressedSummary(groupKey);
- if (callback != null) {
- callback.accept(mBubbleData.getSummaryKey(groupKey));
- }
- }
- }
-
/** Promote the provided bubble from the overflow view. */
public void promoteBubbleFromOverflow(Bubble bubble) {
mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_BACK_TO_STACK);
@@ -1519,14 +1513,15 @@
/**
* Description of current bubble state.
*/
- private void dump(PrintWriter pw, String[] args) {
+ private void dump(PrintWriter pw, String prefix) {
pw.println("BubbleController state:");
- mBubbleData.dump(pw, args);
+ mBubbleData.dump(pw);
pw.println();
if (mStackView != null) {
- mStackView.dump(pw, args);
+ mStackView.dump(pw);
}
pw.println();
+ mImpl.mCachedState.dump(pw);
}
/**
@@ -1711,28 +1706,12 @@
}
@Override
- public boolean isStackExpanded() {
- return mCachedState.isStackExpanded();
- }
-
- @Override
@Nullable
public Bubble getBubbleWithShortcutId(String shortcutId) {
return mCachedState.getBubbleWithShortcutId(shortcutId);
}
@Override
- public void removeSuppressedSummaryIfNecessary(String groupKey, Consumer<String> callback,
- Executor callbackExecutor) {
- mMainExecutor.execute(() -> {
- Consumer<String> cb = callback != null
- ? (key) -> callbackExecutor.execute(() -> callback.accept(key))
- : null;
- BubbleController.this.removeSuppressedSummaryIfNecessary(groupKey, cb);
- });
- }
-
- @Override
public void collapseStack() {
mMainExecutor.execute(() -> {
BubbleController.this.collapseStack();
@@ -1761,13 +1740,6 @@
}
@Override
- public void openBubbleOverflow() {
- mMainExecutor.execute(() -> {
- BubbleController.this.openBubbleOverflow();
- });
- }
-
- @Override
public boolean handleDismissalInterception(BubbleEntry entry,
@Nullable List<BubbleEntry> children, IntConsumer removeCallback,
Executor callbackExecutor) {
@@ -1882,18 +1854,6 @@
mMainExecutor.execute(
() -> BubbleController.this.onNotificationPanelExpandedChanged(expanded));
}
-
- @Override
- public void dump(PrintWriter pw, String[] args) {
- try {
- mMainExecutor.executeBlocking(() -> {
- BubbleController.this.dump(pw, args);
- mCachedState.dump(pw);
- });
- } catch (InterruptedException e) {
- Slog.e(TAG, "Failed to dump BubbleController in 2s");
- }
- }
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
index fa86c84..c64133f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
@@ -1136,7 +1136,7 @@
/**
* Description of current bubble data state.
*/
- public void dump(PrintWriter pw, String[] args) {
+ public void dump(PrintWriter pw) {
pw.print("selected: ");
pw.println(mSelectedBubble != null
? mSelectedBubble.getKey()
@@ -1147,13 +1147,13 @@
pw.print("stack bubble count: ");
pw.println(mBubbles.size());
for (Bubble bubble : mBubbles) {
- bubble.dump(pw, args);
+ bubble.dump(pw);
}
pw.print("overflow bubble count: ");
pw.println(mOverflowBubbles.size());
for (Bubble bubble : mOverflowBubbles) {
- bubble.dump(pw, args);
+ bubble.dump(pw);
}
pw.print("summaryKeys: ");
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index 2666a0e..cfbe1b3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -1044,7 +1044,7 @@
/**
* Description of current expanded view state.
*/
- public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
+ public void dump(@NonNull PrintWriter pw) {
pw.print("BubbleExpandedView");
pw.print(" taskId: "); pw.println(mTaskId);
pw.print(" stackView: "); pw.println(mStackView);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 2d0be06..5bf88b1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -299,7 +299,7 @@
private BubblesNavBarGestureTracker mBubblesNavBarGestureTracker;
/** Description of current animation controller state. */
- public void dump(PrintWriter pw, String[] args) {
+ public void dump(PrintWriter pw) {
pw.println("Stack view state:");
String bubblesOnScreen = BubbleDebugConfig.formatBubblesString(
@@ -313,8 +313,8 @@
pw.print(" expandedContainerMatrix: ");
pw.println(mExpandedViewContainer.getAnimationMatrix());
- mStackAnimationController.dump(pw, args);
- mExpandedAnimationController.dump(pw, args);
+ mStackAnimationController.dump(pw);
+ mExpandedAnimationController.dump(pw);
if (mExpandedBubble != null) {
pw.println("Expanded bubble state:");
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
index 37b96ff..0e97e9e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
@@ -35,7 +35,6 @@
import com.android.wm.shell.common.annotations.ExternalThread;
-import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.HashMap;
@@ -91,18 +90,6 @@
*/
boolean isBubbleExpanded(String key);
- /** @return {@code true} if stack of bubbles is expanded or not. */
- boolean isStackExpanded();
-
- /**
- * Removes a group key indicating that the summary for this group should no longer be
- * suppressed.
- *
- * @param callback If removed, this callback will be called with the summary key of the group
- */
- void removeSuppressedSummaryIfNecessary(String groupKey, Consumer<String> callback,
- Executor callbackExecutor);
-
/** Tell the stack of bubbles to collapse. */
void collapseStack();
@@ -130,9 +117,6 @@
/** Called for any taskbar changes. */
void onTaskbarChanged(Bundle b);
- /** Open the overflow view. */
- void openBubbleOverflow();
-
/**
* We intercept notification entries (including group summaries) dismissed by the user when
* there is an active bubble associated with it. We do this so that developers can still
@@ -252,9 +236,6 @@
*/
void onUserRemoved(int removedUserId);
- /** Description of current bubble state. */
- void dump(PrintWriter pw, String[] args);
-
/** Listener to find out about stack expansion / collapse events. */
interface BubbleExpandListener {
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
index b521cb6a..ae434bc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
@@ -468,7 +468,7 @@
}
/** Description of current animation controller state. */
- public void dump(PrintWriter pw, String[] args) {
+ public void dump(PrintWriter pw) {
pw.println("ExpandedAnimationController state:");
pw.print(" isActive: "); pw.println(isActiveController());
pw.print(" animatingExpand: "); pw.println(mAnimatingExpand);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
index 0a1b4d7..4e2cbfd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
@@ -431,7 +431,7 @@
}
/** Description of current animation controller state. */
- public void dump(PrintWriter pw, String[] args) {
+ public void dump(PrintWriter pw) {
pw.println("StackAnimationController state:");
pw.print(" isActive: "); pw.println(isActiveController());
pw.print(" restingStackPos: ");
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
index e22c951..8022e9b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
@@ -66,6 +66,7 @@
@Provides
static Optional<Pip> providePip(
Context context,
+ ShellInit shellInit,
ShellController shellController,
TvPipBoundsState tvPipBoundsState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
@@ -84,6 +85,7 @@
return Optional.of(
TvPipController.create(
context,
+ shellInit,
shellController,
tvPipBoundsState,
tvPipBoundsAlgorithm,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index a6a04cf..ceaa64e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -627,11 +627,12 @@
@WMSingleton
@Provides
- static ActivityEmbeddingController provideActivityEmbeddingController(
+ static Optional<ActivityEmbeddingController> provideActivityEmbeddingController(
Context context,
ShellInit shellInit,
Transitions transitions) {
- return new ActivityEmbeddingController(context, shellInit, transitions);
+ return Optional.ofNullable(
+ ActivityEmbeddingController.create(context, shellInit, transitions));
}
//
@@ -686,7 +687,7 @@
Optional<RecentTasksController> recentTasksOptional,
Optional<OneHandedController> oneHandedControllerOptional,
Optional<HideDisplayCutoutController> hideDisplayCutoutControllerOptional,
- ActivityEmbeddingController activityEmbeddingOptional,
+ Optional<ActivityEmbeddingController> activityEmbeddingOptional,
Transitions transitions,
StartingWindowController startingWindow,
@ShellCreateTriggerOverride Optional<Object> overriddenCreateTrigger) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 2bcc134..4fe3255 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -145,6 +145,7 @@
@Provides
static BubbleController provideBubbleController(Context context,
ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
ShellController shellController,
BubbleData data,
FloatingContentCoordinator floatingContentCoordinator,
@@ -165,7 +166,7 @@
@ShellBackgroundThread ShellExecutor bgExecutor,
TaskViewTransitions taskViewTransitions,
SyncTransactionQueue syncQueue) {
- return new BubbleController(context, shellInit, shellController, data,
+ return new BubbleController(context, shellInit, shellCommandHandler, shellController, data,
null /* synchronizer */, floatingContentCoordinator,
new BubbleDataRepository(context, launcherApps, mainExecutor),
statusBarService, windowManager, windowManagerShellWrapper, userManager,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java
index 76c0f41..7129165 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java
@@ -37,16 +37,6 @@
}
/**
- * Return one handed settings enabled or not.
- */
- boolean isOneHandedEnabled();
-
- /**
- * Return swipe to notification settings enabled or not.
- */
- boolean isSwipeToNotificationEnabled();
-
- /**
* Enters one handed mode.
*/
void startOneHanded();
@@ -80,9 +70,4 @@
* transition start or finish
*/
void registerTransitionCallback(OneHandedTransitionCallback callback);
-
- /**
- * Notifies when user switch complete
- */
- void onUserSwitch(int userId);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
index 9149204..e0c4fe8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
@@ -59,6 +59,7 @@
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.sysui.UserChangeListener;
import java.io.PrintWriter;
@@ -67,7 +68,7 @@
*/
public class OneHandedController implements RemoteCallable<OneHandedController>,
DisplayChangeController.OnDisplayChangingListener, ConfigurationChangeListener,
- KeyguardChangeListener {
+ KeyguardChangeListener, UserChangeListener {
private static final String TAG = "OneHandedController";
private static final String ONE_HANDED_MODE_OFFSET_PERCENTAGE =
@@ -76,8 +77,8 @@
public static final String SUPPORT_ONE_HANDED_MODE = "ro.support_one_handed_mode";
- private volatile boolean mIsOneHandedEnabled;
- private volatile boolean mIsSwipeToNotificationEnabled;
+ private boolean mIsOneHandedEnabled;
+ private boolean mIsSwipeToNotificationEnabled;
private boolean mIsShortcutEnabled;
private boolean mTaskChangeToExit;
private boolean mLockedDisabled;
@@ -294,6 +295,7 @@
mState.addSListeners(mTutorialHandler);
mShellController.addConfigurationChangeListener(this);
mShellController.addKeyguardChangeListener(this);
+ mShellController.addUserChangeListener(this);
}
public OneHanded asOneHanded() {
@@ -627,7 +629,8 @@
stopOneHanded();
}
- private void onUserSwitch(int newUserId) {
+ @Override
+ public void onUserChanged(int newUserId, @NonNull Context userContext) {
unregisterSettingObservers();
mUserId = newUserId;
registerSettingObservers(newUserId);
@@ -718,18 +721,6 @@
}
@Override
- public boolean isOneHandedEnabled() {
- // This is volatile so return directly
- return mIsOneHandedEnabled;
- }
-
- @Override
- public boolean isSwipeToNotificationEnabled() {
- // This is volatile so return directly
- return mIsSwipeToNotificationEnabled;
- }
-
- @Override
public void startOneHanded() {
mMainExecutor.execute(() -> {
OneHandedController.this.startOneHanded();
@@ -770,13 +761,6 @@
OneHandedController.this.registerTransitionCallback(callback);
});
}
-
- @Override
- public void onUserSwitch(int userId) {
- mMainExecutor.execute(() -> {
- OneHandedController.this.onUserSwitch(userId);
- });
- }
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
index 93172f8..c06881a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
@@ -51,12 +51,6 @@
}
/**
- * Registers the session listener for the current user.
- */
- default void registerSessionListenerForCurrentUser() {
- }
-
- /**
* Sets both shelf visibility and its height.
*
* @param visible visibility of shelf.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index fc97f31..ac3407d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -92,6 +92,7 @@
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.sysui.UserChangeListener;
import com.android.wm.shell.transition.Transitions;
import java.io.PrintWriter;
@@ -105,7 +106,8 @@
* Manages the picture-in-picture (PIP) UI and states for Phones.
*/
public class PipController implements PipTransitionController.PipTransitionCallback,
- RemoteCallable<PipController>, ConfigurationChangeListener, KeyguardChangeListener {
+ RemoteCallable<PipController>, ConfigurationChangeListener, KeyguardChangeListener,
+ UserChangeListener {
private static final String TAG = "PipController";
private Context mContext;
@@ -528,7 +530,7 @@
});
mOneHandedController.ifPresent(controller -> {
- controller.asOneHanded().registerTransitionCallback(
+ controller.registerTransitionCallback(
new OneHandedTransitionCallback() {
@Override
public void onStartFinished(Rect bounds) {
@@ -542,8 +544,11 @@
});
});
+ mMediaController.registerSessionListenerForCurrentUser();
+
mShellController.addConfigurationChangeListener(this);
mShellController.addKeyguardChangeListener(this);
+ mShellController.addUserChangeListener(this);
}
@Override
@@ -557,6 +562,12 @@
}
@Override
+ public void onUserChanged(int newUserId, @NonNull Context userContext) {
+ // Re-register the media session listener when switching users
+ mMediaController.registerSessionListenerForCurrentUser();
+ }
+
+ @Override
public void onConfigurationChanged(Configuration newConfig) {
mPipBoundsAlgorithm.onConfigurationChanged(mContext);
mTouchHandler.onConfigurationChanged();
@@ -644,10 +655,6 @@
}
}
- private void registerSessionListenerForCurrentUser() {
- mMediaController.registerSessionListenerForCurrentUser();
- }
-
private void onSystemUiStateChanged(boolean isValidState, int flag) {
mTouchHandler.onSystemUiStateChanged(isValidState);
}
@@ -968,13 +975,6 @@
}
@Override
- public void registerSessionListenerForCurrentUser() {
- mMainExecutor.execute(() -> {
- PipController.this.registerSessionListenerForCurrentUser();
- });
- }
-
- @Override
public void setShelfHeight(boolean visible, int height) {
mMainExecutor.execute(() -> {
PipController.this.setShelfHeight(visible, height);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
index a24d9618..4e1b046 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
@@ -32,6 +32,8 @@
import android.os.RemoteException;
import android.view.Gravity;
+import androidx.annotation.NonNull;
+
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.R;
import com.android.wm.shell.WindowManagerShellWrapper;
@@ -51,6 +53,8 @@
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.sysui.ConfigurationChangeListener;
import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.sysui.UserChangeListener;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -64,7 +68,7 @@
public class TvPipController implements PipTransitionController.PipTransitionCallback,
TvPipBoundsController.PipBoundsListener, TvPipMenuController.Delegate,
TvPipNotificationController.Delegate, DisplayController.OnDisplaysChangedListener,
- ConfigurationChangeListener {
+ ConfigurationChangeListener, UserChangeListener {
private static final String TAG = "TvPipController";
static final boolean DEBUG = false;
@@ -105,6 +109,11 @@
private final PipMediaController mPipMediaController;
private final TvPipNotificationController mPipNotificationController;
private final TvPipMenuController mTvPipMenuController;
+ private final PipTransitionController mPipTransitionController;
+ private final TaskStackListenerImpl mTaskStackListener;
+ private final PipParamsChangedForwarder mPipParamsChangedForwarder;
+ private final DisplayController mDisplayController;
+ private final WindowManagerShellWrapper mWmShellWrapper;
private final ShellExecutor mMainExecutor;
private final TvPipImpl mImpl = new TvPipImpl();
@@ -121,6 +130,7 @@
public static Pip create(
Context context,
+ ShellInit shellInit,
ShellController shellController,
TvPipBoundsState tvPipBoundsState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
@@ -138,6 +148,7 @@
ShellExecutor mainExecutor) {
return new TvPipController(
context,
+ shellInit,
shellController,
tvPipBoundsState,
tvPipBoundsAlgorithm,
@@ -157,6 +168,7 @@
private TvPipController(
Context context,
+ ShellInit shellInit,
ShellController shellController,
TvPipBoundsState tvPipBoundsState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
@@ -170,11 +182,12 @@
TaskStackListenerImpl taskStackListener,
PipParamsChangedForwarder pipParamsChangedForwarder,
DisplayController displayController,
- WindowManagerShellWrapper wmShell,
+ WindowManagerShellWrapper wmShellWrapper,
ShellExecutor mainExecutor) {
mContext = context;
mMainExecutor = mainExecutor;
mShellController = shellController;
+ mDisplayController = displayController;
mTvPipBoundsState = tvPipBoundsState;
mTvPipBoundsState.setDisplayId(context.getDisplayId());
@@ -193,16 +206,32 @@
mAppOpsListener = pipAppOpsListener;
mPipTaskOrganizer = pipTaskOrganizer;
- pipTransitionController.registerPipTransitionCallback(this);
+ mPipTransitionController = pipTransitionController;
+ mPipParamsChangedForwarder = pipParamsChangedForwarder;
+ mTaskStackListener = taskStackListener;
+ mWmShellWrapper = wmShellWrapper;
+ shellInit.addInitCallback(this::onInit, this);
+ }
+
+ private void onInit() {
+ mPipTransitionController.registerPipTransitionCallback(this);
loadConfigurations();
- registerPipParamsChangedListener(pipParamsChangedForwarder);
- registerTaskStackListenerCallback(taskStackListener);
- registerWmShellPinnedStackListener(wmShell);
- displayController.addDisplayWindowListener(this);
+ registerPipParamsChangedListener(mPipParamsChangedForwarder);
+ registerTaskStackListenerCallback(mTaskStackListener);
+ registerWmShellPinnedStackListener(mWmShellWrapper);
+ registerSessionListenerForCurrentUser();
+ mDisplayController.addDisplayWindowListener(this);
mShellController.addConfigurationChangeListener(this);
+ mShellController.addUserChangeListener(this);
+ }
+
+ @Override
+ public void onUserChanged(int newUserId, @NonNull Context userContext) {
+ // Re-register the media session listener when switching users
+ registerSessionListenerForCurrentUser();
}
@Override
@@ -679,11 +708,6 @@
}
private class TvPipImpl implements Pip {
- @Override
- public void registerSessionListenerForCurrentUser() {
- mMainExecutor.execute(() -> {
- TvPipController.this.registerSessionListenerForCurrentUser();
- });
- }
+ // Not used
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/KeyguardChangeListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/KeyguardChangeListener.java
index 1c0b358..9df8631 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/KeyguardChangeListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/KeyguardChangeListener.java
@@ -21,13 +21,13 @@
*/
public interface KeyguardChangeListener {
/**
- * Notifies the Shell that the keyguard is showing (and if so, whether it is occluded).
+ * Called when the keyguard is showing (and if so, whether it is occluded).
*/
default void onKeyguardVisibilityChanged(boolean visible, boolean occluded,
boolean animatingDismiss) {}
/**
- * Notifies the Shell when the keyguard dismiss animation has finished.
+ * Called when the keyguard dismiss animation has finished.
*
* TODO(b/206741900) deprecate this path once we're able to animate the PiP window as part of
* keyguard dismiss animation.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
index 52ffb46..5799394 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
@@ -25,7 +25,9 @@
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SYSUI_EVENTS;
+import android.content.Context;
import android.content.pm.ActivityInfo;
+import android.content.pm.UserInfo;
import android.content.res.Configuration;
import androidx.annotation.NonNull;
@@ -36,6 +38,7 @@
import com.android.wm.shell.common.annotations.ExternalThread;
import java.io.PrintWriter;
+import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
@@ -53,6 +56,9 @@
new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<KeyguardChangeListener> mKeyguardChangeListeners =
new CopyOnWriteArrayList<>();
+ private final CopyOnWriteArrayList<UserChangeListener> mUserChangeListeners =
+ new CopyOnWriteArrayList<>();
+
private Configuration mLastConfiguration;
@@ -102,6 +108,22 @@
mKeyguardChangeListeners.remove(listener);
}
+ /**
+ * Adds a new user-change listener. The user change callbacks are not made in any
+ * particular order.
+ */
+ public void addUserChangeListener(UserChangeListener listener) {
+ mUserChangeListeners.remove(listener);
+ mUserChangeListeners.add(listener);
+ }
+
+ /**
+ * Removes an existing user-change listener.
+ */
+ public void removeUserChangeListener(UserChangeListener listener) {
+ mUserChangeListeners.remove(listener);
+ }
+
@VisibleForTesting
void onConfigurationChanged(Configuration newConfig) {
// The initial config is send on startup and doesn't trigger listener callbacks
@@ -144,6 +166,8 @@
@VisibleForTesting
void onKeyguardVisibilityChanged(boolean visible, boolean occluded, boolean animatingDismiss) {
+ ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Keyguard visibility changed: visible=%b "
+ + "occluded=%b animatingDismiss=%b", visible, occluded, animatingDismiss);
for (KeyguardChangeListener listener : mKeyguardChangeListeners) {
listener.onKeyguardVisibilityChanged(visible, occluded, animatingDismiss);
}
@@ -151,17 +175,35 @@
@VisibleForTesting
void onKeyguardDismissAnimationFinished() {
+ ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Keyguard dismiss animation finished");
for (KeyguardChangeListener listener : mKeyguardChangeListeners) {
listener.onKeyguardDismissAnimationFinished();
}
}
+ @VisibleForTesting
+ void onUserChanged(int newUserId, @NonNull Context userContext) {
+ ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "User changed: id=%d", newUserId);
+ for (UserChangeListener listener : mUserChangeListeners) {
+ listener.onUserChanged(newUserId, userContext);
+ }
+ }
+
+ @VisibleForTesting
+ void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {
+ ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "User profiles changed");
+ for (UserChangeListener listener : mUserChangeListeners) {
+ listener.onUserProfilesChanged(profiles);
+ }
+ }
+
public void dump(@NonNull PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
pw.println(prefix + TAG);
pw.println(innerPrefix + "mConfigChangeListeners=" + mConfigChangeListeners.size());
pw.println(innerPrefix + "mLastConfiguration=" + mLastConfiguration);
pw.println(innerPrefix + "mKeyguardChangeListeners=" + mKeyguardChangeListeners.size());
+ pw.println(innerPrefix + "mUserChangeListeners=" + mUserChangeListeners.size());
}
/**
@@ -220,5 +262,17 @@
mMainExecutor.execute(() ->
ShellController.this.onKeyguardDismissAnimationFinished());
}
+
+ @Override
+ public void onUserChanged(int newUserId, @NonNull Context userContext) {
+ mMainExecutor.execute(() ->
+ ShellController.this.onUserChanged(newUserId, userContext));
+ }
+
+ @Override
+ public void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {
+ mMainExecutor.execute(() ->
+ ShellController.this.onUserProfilesChanged(profiles));
+ }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java
index 254c253..2108c82 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java
@@ -16,9 +16,14 @@
package com.android.wm.shell.sysui;
+import android.content.Context;
+import android.content.pm.UserInfo;
import android.content.res.Configuration;
+import androidx.annotation.NonNull;
+
import java.io.PrintWriter;
+import java.util.List;
/**
* General interface for notifying the Shell of common SysUI events like configuration or keyguard
@@ -59,4 +64,14 @@
* Notifies the Shell when the keyguard dismiss animation has finished.
*/
default void onKeyguardDismissAnimationFinished() {}
+
+ /**
+ * Notifies the Shell when the user changes.
+ */
+ default void onUserChanged(int newUserId, @NonNull Context userContext) {}
+
+ /**
+ * Notifies the Shell when a profile belonging to the user changes.
+ */
+ default void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/UserChangeListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/UserChangeListener.java
new file mode 100644
index 0000000..3d0909f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/UserChangeListener.java
@@ -0,0 +1,39 @@
+/*
+ * 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.wm.shell.sysui;
+
+import android.content.Context;
+import android.content.pm.UserInfo;
+
+import androidx.annotation.NonNull;
+
+import java.util.List;
+
+/**
+ * Callbacks for when the user or user's profiles changes.
+ */
+public interface UserChangeListener {
+ /**
+ * Called when the current (parent) user changes.
+ */
+ default void onUserChanged(int newUserId, @NonNull Context userContext) {}
+
+ /**
+ * Called when a profile belonging to the user changes.
+ */
+ default void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {}
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 26d0ec6..29d25bc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -219,6 +219,8 @@
+ "use ShellInit callbacks to ensure proper ordering");
}
mHandlers.add(handler);
+ // Set initial scale settings.
+ handler.setAnimScaleSetting(mTransitionAnimationScaleSetting);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: %s",
handler.getClass().getSimpleName());
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
new file mode 100644
index 0000000..b2e45a6
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
@@ -0,0 +1,79 @@
+/*
+ * 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.wm.shell.activityembedding;
+
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.window.TransitionInfo.FLAG_IS_EMBEDDED;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.window.TransitionInfo;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+/**
+ * Tests for {@link ActivityEmbeddingAnimationRunner}.
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:ActivityEmbeddingAnimationRunnerTests
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnimationTestBase {
+
+ @Before
+ public void setup() {
+ super.setUp();
+ doNothing().when(mController).onAnimationFinished(any());
+ }
+
+ @Test
+ public void testStartAnimation() {
+ final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
+ final TransitionInfo.Change embeddingChange = createChange();
+ embeddingChange.setFlags(FLAG_IS_EMBEDDED);
+ info.addChange(embeddingChange);
+ doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any());
+
+ mAnimRunner.startAnimation(mTransition, info, mStartTransaction, mFinishTransaction);
+
+ final ArgumentCaptor<Runnable> finishCallback = ArgumentCaptor.forClass(Runnable.class);
+ verify(mAnimRunner).createAnimator(eq(info), eq(mStartTransaction), eq(mFinishTransaction),
+ finishCallback.capture());
+ verify(mStartTransaction).apply();
+ verify(mAnimator).start();
+ verifyNoMoreInteractions(mFinishTransaction);
+ verify(mController, never()).onAnimationFinished(any());
+
+ // Call onAnimationFinished() when the animation is finished.
+ finishCallback.getValue().run();
+
+ verify(mController).onAnimationFinished(mTransition);
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
new file mode 100644
index 0000000..84befdd
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
@@ -0,0 +1,83 @@
+/*
+ * 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.wm.shell.activityembedding;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.Mockito.mock;
+
+import android.animation.Animator;
+import android.annotation.CallSuper;
+import android.os.IBinder;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+import android.window.WindowContainerToken;
+
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.Transitions;
+
+import org.junit.Before;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** TestBase for ActivityEmbedding animation. */
+abstract class ActivityEmbeddingAnimationTestBase extends ShellTestCase {
+
+ @Mock
+ ShellInit mShellInit;
+ @Mock
+ Transitions mTransitions;
+ @Mock
+ IBinder mTransition;
+ @Mock
+ SurfaceControl.Transaction mStartTransaction;
+ @Mock
+ SurfaceControl.Transaction mFinishTransaction;
+ @Mock
+ Transitions.TransitionFinishCallback mFinishCallback;
+ @Mock
+ Animator mAnimator;
+
+ ActivityEmbeddingController mController;
+ ActivityEmbeddingAnimationRunner mAnimRunner;
+ ActivityEmbeddingAnimationSpec mAnimSpec;
+
+ @CallSuper
+ @Before
+ public void setUp() {
+ assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
+ MockitoAnnotations.initMocks(this);
+ mController = ActivityEmbeddingController.create(mContext, mShellInit, mTransitions);
+ assertNotNull(mController);
+ mAnimRunner = mController.mAnimationRunner;
+ assertNotNull(mAnimRunner);
+ mAnimSpec = mAnimRunner.mAnimationSpec;
+ assertNotNull(mAnimSpec);
+ spyOn(mController);
+ spyOn(mAnimRunner);
+ spyOn(mAnimSpec);
+ }
+
+ /** Creates a mock {@link TransitionInfo.Change}. */
+ static TransitionInfo.Change createChange() {
+ return new TransitionInfo.Change(mock(WindowContainerToken.class),
+ mock(SurfaceControl.class));
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
index bfe3b54..cf43b00 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
@@ -16,52 +16,117 @@
package com.android.wm.shell.activityembedding;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.window.TransitionInfo.FLAG_IS_EMBEDDED;
-import static org.junit.Assume.assumeTrue;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
-import android.content.Context;
+import android.window.TransitionInfo;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import com.android.wm.shell.ShellTestCase;
-import com.android.wm.shell.sysui.ShellInit;
-import com.android.wm.shell.transition.Transitions;
-
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
/**
- * Tests for the activity embedding controller.
+ * Tests for {@link ActivityEmbeddingController}.
*
* Build/Install/Run:
* atest WMShellUnitTests:ActivityEmbeddingControllerTests
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
-public class ActivityEmbeddingControllerTests extends ShellTestCase {
-
- private @Mock Context mContext;
- private @Mock ShellInit mShellInit;
- private @Mock Transitions mTransitions;
- private ActivityEmbeddingController mController;
+public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimationTestBase {
@Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- mController = spy(new ActivityEmbeddingController(mContext, mShellInit, mTransitions));
+ public void setup() {
+ super.setUp();
+ doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any());
}
@Test
- public void instantiate_addInitCallback() {
- assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
- verify(mShellInit, times(1)).addInitCallback(any(), any());
+ public void testInstantiate() {
+ verify(mShellInit).addInitCallback(any(), any());
+ }
+
+ @Test
+ public void testOnInit() {
+ mController.onInit();
+
+ verify(mTransitions).addHandler(mController);
+ }
+
+ @Test
+ public void testSetAnimScaleSetting() {
+ mController.setAnimScaleSetting(1.0f);
+
+ verify(mAnimRunner).setAnimScaleSetting(1.0f);
+ verify(mAnimSpec).setAnimScaleSetting(1.0f);
+ }
+
+ @Test
+ public void testStartAnimation_containsNonActivityEmbeddingChange() {
+ final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
+ final TransitionInfo.Change embeddingChange = createChange();
+ embeddingChange.setFlags(FLAG_IS_EMBEDDED);
+ final TransitionInfo.Change nonEmbeddingChange = createChange();
+ info.addChange(embeddingChange);
+ info.addChange(nonEmbeddingChange);
+
+ // No-op
+ assertFalse(mController.startAnimation(mTransition, info, mStartTransaction,
+ mFinishTransaction, mFinishCallback));
+ verify(mAnimRunner, never()).startAnimation(any(), any(), any(), any());
+ verifyNoMoreInteractions(mStartTransaction);
+ verifyNoMoreInteractions(mFinishTransaction);
+ verifyNoMoreInteractions(mFinishCallback);
+ }
+
+ @Test
+ public void testStartAnimation_onlyActivityEmbeddingChange() {
+ final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
+ final TransitionInfo.Change embeddingChange = createChange();
+ embeddingChange.setFlags(FLAG_IS_EMBEDDED);
+ info.addChange(embeddingChange);
+
+ // No-op
+ assertTrue(mController.startAnimation(mTransition, info, mStartTransaction,
+ mFinishTransaction, mFinishCallback));
+ verify(mAnimRunner).startAnimation(mTransition, info, mStartTransaction,
+ mFinishTransaction);
+ verify(mStartTransaction).apply();
+ verifyNoMoreInteractions(mFinishTransaction);
+ }
+
+ @Test
+ public void testOnAnimationFinished() {
+ // Should not call finish when there is no transition.
+ assertThrows(IllegalStateException.class,
+ () -> mController.onAnimationFinished(mTransition));
+
+ final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
+ final TransitionInfo.Change embeddingChange = createChange();
+ embeddingChange.setFlags(FLAG_IS_EMBEDDED);
+ info.addChange(embeddingChange);
+ mController.startAnimation(mTransition, info, mStartTransaction,
+ mFinishTransaction, mFinishCallback);
+
+ verify(mFinishCallback, never()).onTransitionFinished(any(), any());
+ mController.onAnimationFinished(mTransition);
+ verify(mFinishCallback).onTransitionFinished(any(), any());
+
+ // Should not call finish when the finish has already been called.
+ assertThrows(IllegalStateException.class,
+ () -> mController.onAnimationFinished(mTransition));
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java
index 90645ce..cf8297e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java
@@ -171,6 +171,11 @@
}
@Test
+ public void testControllerRegistersUserChangeListener() {
+ verify(mMockShellController, times(1)).addUserChangeListener(any());
+ }
+
+ @Test
public void testDefaultShouldNotInOneHanded() {
// Assert default transition state is STATE_NONE
assertThat(mSpiedTransitionState.getState()).isEqualTo(STATE_NONE);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
index 9ed8d84..eb5726b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
@@ -23,6 +23,7 @@
import static org.mockito.Mockito.doAnswer;
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.times;
import static org.mockito.Mockito.verify;
@@ -77,9 +78,9 @@
public class PipControllerTest extends ShellTestCase {
private PipController mPipController;
private ShellInit mShellInit;
+ private ShellController mShellController;
@Mock private ShellCommandHandler mMockShellCommandHandler;
- @Mock private ShellController mMockShellController;
@Mock private DisplayController mMockDisplayController;
@Mock private PhonePipMenuController mMockPhonePipMenuController;
@Mock private PipAppOpsListener mMockPipAppOpsListener;
@@ -110,8 +111,10 @@
return null;
}).when(mMockExecutor).execute(any());
mShellInit = spy(new ShellInit(mMockExecutor));
+ mShellController = spy(new ShellController(mShellInit, mMockShellCommandHandler,
+ mMockExecutor));
mPipController = new PipController(mContext, mShellInit, mMockShellCommandHandler,
- mMockShellController, mMockDisplayController, mMockPipAppOpsListener,
+ mShellController, mMockDisplayController, mMockPipAppOpsListener,
mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm,
mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController,
mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTransitionState,
@@ -135,12 +138,22 @@
@Test
public void instantiatePipController_registerConfigChangeListener() {
- verify(mMockShellController, times(1)).addConfigurationChangeListener(any());
+ verify(mShellController, times(1)).addConfigurationChangeListener(any());
}
@Test
public void instantiatePipController_registerKeyguardChangeListener() {
- verify(mMockShellController, times(1)).addKeyguardChangeListener(any());
+ verify(mShellController, times(1)).addKeyguardChangeListener(any());
+ }
+
+ @Test
+ public void instantiatePipController_registerUserChangeListener() {
+ verify(mShellController, times(1)).addUserChangeListener(any());
+ }
+
+ @Test
+ public void instantiatePipController_registerMediaListener() {
+ verify(mMockPipMediaController, times(1)).registerSessionListenerForCurrentUser();
}
@Test
@@ -167,7 +180,7 @@
ShellInit shellInit = new ShellInit(mMockExecutor);
assertNull(PipController.create(spyContext, shellInit, mMockShellCommandHandler,
- mMockShellController, mMockDisplayController, mMockPipAppOpsListener,
+ mShellController, mMockDisplayController, mMockPipAppOpsListener,
mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm,
mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController,
mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTransitionState,
@@ -264,4 +277,11 @@
verify(mMockPipBoundsState).setKeepClearAreas(Set.of(keepClearArea), Set.of());
}
+
+ @Test
+ public void onUserChangeRegisterMediaListener() {
+ reset(mMockPipMediaController);
+ mShellController.asShell().onUserChanged(100, mContext);
+ verify(mMockPipMediaController, times(1)).registerSessionListenerForCurrentUser();
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java
index 39e58ff..d6ddba9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java
@@ -17,11 +17,15 @@
package com.android.wm.shell.sysui;
import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import android.content.Context;
+import android.content.pm.UserInfo;
import android.content.res.Configuration;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
+import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -35,6 +39,8 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Locale;
@SmallTest
@@ -42,22 +48,29 @@
@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class ShellControllerTest extends ShellTestCase {
+ private static final int TEST_USER_ID = 100;
+
@Mock
private ShellInit mShellInit;
@Mock
private ShellCommandHandler mShellCommandHandler;
@Mock
private ShellExecutor mExecutor;
+ @Mock
+ private Context mTestUserContext;
private ShellController mController;
private TestConfigurationChangeListener mConfigChangeListener;
private TestKeyguardChangeListener mKeyguardChangeListener;
+ private TestUserChangeListener mUserChangeListener;
+
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mKeyguardChangeListener = new TestKeyguardChangeListener();
mConfigChangeListener = new TestConfigurationChangeListener();
+ mUserChangeListener = new TestUserChangeListener();
mController = new ShellController(mShellInit, mShellCommandHandler, mExecutor);
mController.onConfigurationChanged(getConfigurationCopy());
}
@@ -68,6 +81,46 @@
}
@Test
+ public void testAddUserChangeListener_ensureCallback() {
+ mController.addUserChangeListener(mUserChangeListener);
+
+ mController.onUserChanged(TEST_USER_ID, mTestUserContext);
+ assertTrue(mUserChangeListener.userChanged == 1);
+ assertTrue(mUserChangeListener.lastUserContext == mTestUserContext);
+ }
+
+ @Test
+ public void testDoubleAddUserChangeListener_ensureSingleCallback() {
+ mController.addUserChangeListener(mUserChangeListener);
+ mController.addUserChangeListener(mUserChangeListener);
+
+ mController.onUserChanged(TEST_USER_ID, mTestUserContext);
+ assertTrue(mUserChangeListener.userChanged == 1);
+ assertTrue(mUserChangeListener.lastUserContext == mTestUserContext);
+ }
+
+ @Test
+ public void testAddRemoveUserChangeListener_ensureNoCallback() {
+ mController.addUserChangeListener(mUserChangeListener);
+ mController.removeUserChangeListener(mUserChangeListener);
+
+ mController.onUserChanged(TEST_USER_ID, mTestUserContext);
+ assertTrue(mUserChangeListener.userChanged == 0);
+ assertTrue(mUserChangeListener.lastUserContext == null);
+ }
+
+ @Test
+ public void testUserProfilesChanged() {
+ mController.addUserChangeListener(mUserChangeListener);
+
+ ArrayList<UserInfo> profiles = new ArrayList<>();
+ profiles.add(mock(UserInfo.class));
+ profiles.add(mock(UserInfo.class));
+ mController.onUserProfilesChanged(profiles);
+ assertTrue(mUserChangeListener.lastUserProfiles.equals(profiles));
+ }
+
+ @Test
public void testAddKeyguardChangeListener_ensureCallback() {
mController.addKeyguardChangeListener(mKeyguardChangeListener);
@@ -332,4 +385,27 @@
dismissAnimationFinished++;
}
}
+
+ private class TestUserChangeListener implements UserChangeListener {
+ // Counts of number of times each of the callbacks are called
+ public int userChanged;
+ public int lastUserId;
+ public Context lastUserContext;
+ public int userProfilesChanged;
+ public List<? extends UserInfo> lastUserProfiles;
+
+
+ @Override
+ public void onUserChanged(int newUserId, @NonNull Context userContext) {
+ userChanged++;
+ lastUserId = newUserId;
+ lastUserContext = userContext;
+ }
+
+ @Override
+ public void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {
+ userProfilesChanged++;
+ lastUserProfiles = profiles;
+ }
+ }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java
index b9c4030..a822e18 100644
--- a/packages/SettingsLib/src/com/android/settingslib/Utils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java
@@ -600,6 +600,9 @@
* Returns the WifiInfo for the underlying WiFi network of the VCN network, returns null if the
* input NetworkCapabilities is not for a VCN network with underlying WiFi network.
*
+ * TODO(b/238425913): Move this method to be inside systemui not settingslib once we've migrated
+ * off of {@link WifiStatusTracker} and {@link NetworkControllerImpl}.
+ *
* @param networkCapabilities NetworkCapabilities of the network.
*/
@Nullable
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 652e281..78dea89 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -85,6 +85,7 @@
<uses-permission android:name="android.permission.CONTROL_VPN" />
<uses-permission android:name="android.permission.PEERS_MAC_ADDRESS"/>
<uses-permission android:name="android.permission.READ_WIFI_CREDENTIAL"/>
+ <uses-permission android:name="android.permission.NETWORK_STACK"/>
<!-- Physical hardware -->
<uses-permission android:name="android.permission.MANAGE_USB" />
<uses-permission android:name="android.permission.CONTROL_DISPLAY_BRIGHTNESS" />
diff --git a/packages/SystemUI/res-keyguard/values-land/dimens.xml b/packages/SystemUI/res-keyguard/values-land/dimens.xml
index 4e92884f..a4e7a5f 100644
--- a/packages/SystemUI/res-keyguard/values-land/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values-land/dimens.xml
@@ -22,7 +22,6 @@
<dimen name="keyguard_eca_top_margin">0dp</dimen>
<dimen name="keyguard_eca_bottom_margin">2dp</dimen>
<dimen name="keyguard_password_height">26dp</dimen>
- <dimen name="num_pad_entry_row_margin_bottom">0dp</dimen>
<!-- The size of PIN text in the PIN unlock method. -->
<integer name="scaled_password_text_size">26</integer>
diff --git a/packages/SystemUI/res-keyguard/values-sw360dp-land/dimens.xml b/packages/SystemUI/res-keyguard/values-sw360dp-land/dimens.xml
index f465be4..0421135 100644
--- a/packages/SystemUI/res-keyguard/values-sw360dp-land/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values-sw360dp-land/dimens.xml
@@ -22,7 +22,6 @@
<dimen name="keyguard_eca_top_margin">4dp</dimen>
<dimen name="keyguard_eca_bottom_margin">4dp</dimen>
<dimen name="keyguard_password_height">50dp</dimen>
- <dimen name="num_pad_entry_row_margin_bottom">4dp</dimen>
<!-- The size of PIN text in the PIN unlock method. -->
<integer name="scaled_password_text_size">40</integer>
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index acf3e4d..32871f0 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -86,7 +86,7 @@
<!-- Spacing around each button used for PIN view -->
<dimen name="num_pad_key_width">72dp</dimen>
- <dimen name="num_pad_entry_row_margin_bottom">16dp</dimen>
+ <dimen name="num_pad_entry_row_margin_bottom">12dp</dimen>
<dimen name="num_pad_row_margin_bottom">6dp</dimen>
<dimen name="num_pad_key_margin_end">12dp</dimen>
diff --git a/packages/SystemUI/res/layout/new_status_bar_wifi_group.xml b/packages/SystemUI/res/layout/new_status_bar_wifi_group.xml
new file mode 100644
index 0000000..753ba2f
--- /dev/null
+++ b/packages/SystemUI/res/layout/new_status_bar_wifi_group.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 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.
+*/
+-->
+<com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/wifi_combo"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical" >
+
+ <include layout="@layout/status_bar_wifi_group_inner" />
+
+</com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView>
diff --git a/packages/SystemUI/res/layout/status_bar_wifi_group.xml b/packages/SystemUI/res/layout/status_bar_wifi_group.xml
index 35cce25..6cb6993b 100644
--- a/packages/SystemUI/res/layout/status_bar_wifi_group.xml
+++ b/packages/SystemUI/res/layout/status_bar_wifi_group.xml
@@ -18,70 +18,11 @@
-->
<com.android.systemui.statusbar.StatusBarWifiView
xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:systemui="http://schemas.android.com/apk/res-auto"
android:id="@+id/wifi_combo"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical" >
- <com.android.keyguard.AlphaOptimizedLinearLayout
- android:id="@+id/wifi_group"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:gravity="center_vertical"
- android:layout_marginStart="2.5dp"
- >
- <FrameLayout
- android:id="@+id/inout_container"
- android:layout_height="17dp"
- android:layout_width="wrap_content"
- android:gravity="center_vertical" >
- <ImageView
- android:id="@+id/wifi_in"
- android:layout_height="wrap_content"
- android:layout_width="wrap_content"
- android:src="@drawable/ic_activity_down"
- android:visibility="gone"
- android:paddingEnd="2dp"
- />
- <ImageView
- android:id="@+id/wifi_out"
- android:layout_height="wrap_content"
- android:layout_width="wrap_content"
- android:src="@drawable/ic_activity_up"
- android:paddingEnd="2dp"
- android:visibility="gone"
- />
- </FrameLayout>
- <FrameLayout
- android:id="@+id/wifi_combo"
- android:layout_height="wrap_content"
- android:layout_width="wrap_content"
- android:gravity="center_vertical" >
- <com.android.systemui.statusbar.AlphaOptimizedImageView
- android:id="@+id/wifi_signal"
- android:layout_height="@dimen/status_bar_wifi_signal_size"
- android:layout_width="@dimen/status_bar_wifi_signal_size" />
- </FrameLayout>
+ <include layout="@layout/status_bar_wifi_group_inner" />
- <View
- android:id="@+id/wifi_signal_spacer"
- android:layout_width="@dimen/status_bar_wifi_signal_spacer_width"
- android:layout_height="4dp"
- android:visibility="gone" />
-
- <!-- Looks like CarStatusBar uses this... -->
- <ViewStub
- android:id="@+id/connected_device_signals_stub"
- android:layout="@layout/connected_device_signal"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
-
- <View
- android:id="@+id/wifi_airplane_spacer"
- android:layout_width="@dimen/status_bar_airplane_spacer_width"
- android:layout_height="4dp"
- android:visibility="gone"
- />
- </com.android.keyguard.AlphaOptimizedLinearLayout>
-</com.android.systemui.statusbar.StatusBarWifiView>
+</com.android.systemui.statusbar.StatusBarWifiView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/status_bar_wifi_group_inner.xml b/packages/SystemUI/res/layout/status_bar_wifi_group_inner.xml
new file mode 100644
index 0000000..0ea0653
--- /dev/null
+++ b/packages/SystemUI/res/layout/status_bar_wifi_group_inner.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 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.
+*/
+-->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <com.android.keyguard.AlphaOptimizedLinearLayout
+ android:id="@+id/wifi_group"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:layout_marginStart="2.5dp"
+ >
+ <FrameLayout
+ android:id="@+id/inout_container"
+ android:layout_height="17dp"
+ android:layout_width="wrap_content"
+ android:gravity="center_vertical" >
+ <ImageView
+ android:id="@+id/wifi_in"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:src="@drawable/ic_activity_down"
+ android:visibility="gone"
+ android:paddingEnd="2dp"
+ />
+ <ImageView
+ android:id="@+id/wifi_out"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:src="@drawable/ic_activity_up"
+ android:paddingEnd="2dp"
+ android:visibility="gone"
+ />
+ </FrameLayout>
+ <FrameLayout
+ android:id="@+id/wifi_combo"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:gravity="center_vertical" >
+ <com.android.systemui.statusbar.AlphaOptimizedImageView
+ android:id="@+id/wifi_signal"
+ android:layout_height="@dimen/status_bar_wifi_signal_size"
+ android:layout_width="@dimen/status_bar_wifi_signal_size" />
+ </FrameLayout>
+
+ <View
+ android:id="@+id/wifi_signal_spacer"
+ android:layout_width="@dimen/status_bar_wifi_signal_spacer_width"
+ android:layout_height="4dp"
+ android:visibility="gone" />
+
+ <!-- Looks like CarStatusBar uses this... -->
+ <ViewStub
+ android:id="@+id/connected_device_signals_stub"
+ android:layout="@layout/connected_device_signal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ <View
+ android:id="@+id/wifi_airplane_spacer"
+ android:layout_width="@dimen/status_bar_airplane_spacer_width"
+ android:layout_height="4dp"
+ android:visibility="gone"
+ />
+ </com.android.keyguard.AlphaOptimizedLinearLayout>
+</merge>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 7c1fdd5..ae30089 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1179,7 +1179,6 @@
<item name="shutdown_scrim_behind_alpha" format="float" type="dimen">0.95</item>
<!-- Output switcher panel related dimensions -->
- <dimen name="media_output_dialog_list_margin">12dp</dimen>
<dimen name="media_output_dialog_list_max_height">355dp</dimen>
<dimen name="media_output_dialog_header_album_icon_size">72dp</dimen>
<dimen name="media_output_dialog_header_back_icon_size">32dp</dimen>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
index b29dc83..22bffda 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
@@ -49,6 +49,7 @@
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.logging.UiEventLoggerImpl;
@@ -99,6 +100,7 @@
private @WindowInsetsController.Behavior
int mBehavior = WindowInsetsController.BEHAVIOR_DEFAULT;
private int mNavBarMode;
+ private boolean mTaskBarVisible = false;
private boolean mSkipOverrideUserLockPrefsOnce;
private final int mLightIconColor;
private final int mDarkIconColor;
@@ -422,6 +424,7 @@
}
public void onTaskbarStateChange(boolean visible, boolean stashed) {
+ mTaskBarVisible = visible;
if (getRotationButton() == null) {
return;
}
@@ -438,9 +441,12 @@
* Return true when either the task bar is visible or it's in visual immersive mode.
*/
@SuppressLint("InlinedApi")
- private boolean canShowRotationButton() {
- return mIsNavigationBarShowing || mBehavior == WindowInsetsController.BEHAVIOR_DEFAULT
- || isGesturalMode(mNavBarMode);
+ @VisibleForTesting
+ boolean canShowRotationButton() {
+ return mIsNavigationBarShowing
+ || mBehavior == WindowInsetsController.BEHAVIOR_DEFAULT
+ || isGesturalMode(mNavBarMode)
+ || mTaskBarVisible;
}
@DrawableRes
@@ -624,4 +630,3 @@
}
}
}
-
diff --git a/packages/SystemUI/src/com/android/systemui/ActivityIntentHelper.java b/packages/SystemUI/src/com/android/systemui/ActivityIntentHelper.java
index 43b3929..df65bcf 100644
--- a/packages/SystemUI/src/com/android/systemui/ActivityIntentHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/ActivityIntentHelper.java
@@ -16,6 +16,7 @@
package com.android.systemui;
+import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
@@ -34,12 +35,12 @@
@SysUISingleton
public class ActivityIntentHelper {
- private final Context mContext;
+ private final PackageManager mPm;
@Inject
public ActivityIntentHelper(Context context) {
// TODO: inject a package manager, not a context.
- mContext = context;
+ mPm = context.getPackageManager();
}
/**
@@ -57,6 +58,15 @@
}
/**
+ * @see #wouldLaunchResolverActivity(Intent, int)
+ */
+ public boolean wouldPendingLaunchResolverActivity(PendingIntent intent, int currentUserId) {
+ ActivityInfo targetActivityInfo = getPendingTargetActivityInfo(intent, currentUserId,
+ false /* onlyDirectBootAware */);
+ return targetActivityInfo == null;
+ }
+
+ /**
* Returns info about the target Activity of a given intent, or null if the intent does not
* resolve to a specific component meeting the requirements.
*
@@ -68,19 +78,45 @@
*/
public ActivityInfo getTargetActivityInfo(Intent intent, int currentUserId,
boolean onlyDirectBootAware) {
- PackageManager packageManager = mContext.getPackageManager();
- int flags = PackageManager.MATCH_DEFAULT_ONLY;
+ int flags = PackageManager.MATCH_DEFAULT_ONLY | PackageManager.GET_META_DATA;
if (!onlyDirectBootAware) {
flags |= PackageManager.MATCH_DIRECT_BOOT_AWARE
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
}
- final List<ResolveInfo> appList = packageManager.queryIntentActivitiesAsUser(
+ final List<ResolveInfo> appList = mPm.queryIntentActivitiesAsUser(
intent, flags, currentUserId);
if (appList.size() == 0) {
return null;
}
- ResolveInfo resolved = packageManager.resolveActivityAsUser(intent,
- flags | PackageManager.GET_META_DATA, currentUserId);
+ if (appList.size() == 1) {
+ return appList.get(0).activityInfo;
+ }
+ ResolveInfo resolved = mPm.resolveActivityAsUser(intent, flags, currentUserId);
+ if (resolved == null || wouldLaunchResolverActivity(resolved, appList)) {
+ return null;
+ } else {
+ return resolved.activityInfo;
+ }
+ }
+
+ /**
+ * @see #getTargetActivityInfo(Intent, int, boolean)
+ */
+ public ActivityInfo getPendingTargetActivityInfo(PendingIntent intent, int currentUserId,
+ boolean onlyDirectBootAware) {
+ int flags = PackageManager.MATCH_DEFAULT_ONLY | PackageManager.GET_META_DATA;
+ if (!onlyDirectBootAware) {
+ flags |= PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
+ }
+ final List<ResolveInfo> appList = intent.queryIntentComponents(flags);
+ if (appList.size() == 0) {
+ return null;
+ }
+ if (appList.size() == 1) {
+ return appList.get(0).activityInfo;
+ }
+ ResolveInfo resolved = mPm.resolveActivityAsUser(intent.getIntent(), flags, currentUserId);
if (resolved == null || wouldLaunchResolverActivity(resolved, appList)) {
return null;
} else {
@@ -104,6 +140,17 @@
}
/**
+ * @see #wouldShowOverLockscreen(Intent, int)
+ */
+ public boolean wouldPendingShowOverLockscreen(PendingIntent intent, int currentUserId) {
+ ActivityInfo targetActivityInfo = getPendingTargetActivityInfo(intent,
+ currentUserId, false /* onlyDirectBootAware */);
+ return targetActivityInfo != null
+ && (targetActivityInfo.flags & (ActivityInfo.FLAG_SHOW_WHEN_LOCKED
+ | ActivityInfo.FLAG_SHOW_FOR_ALL_USERS)) > 0;
+ }
+
+ /**
* Determines if sending the given intent would result in starting an Intent resolver activity,
* instead of resolving to a specific component.
*
diff --git a/packages/SystemUI/src/com/android/systemui/GuestSessionNotification.java b/packages/SystemUI/src/com/android/systemui/GuestSessionNotification.java
index b0eaab9..fa9a83e 100644
--- a/packages/SystemUI/src/com/android/systemui/GuestSessionNotification.java
+++ b/packages/SystemUI/src/com/android/systemui/GuestSessionNotification.java
@@ -25,7 +25,6 @@
import android.os.Bundle;
import android.os.UserHandle;
import android.provider.Settings;
-import android.util.FeatureFlagUtils;
import com.android.internal.messages.nano.SystemMessageProto;
import com.android.systemui.util.NotificationChannels;
@@ -59,10 +58,8 @@
}
void createPersistentNotification(UserInfo userInfo, boolean isGuestFirstLogin) {
- if (!FeatureFlagUtils.isEnabled(mContext,
- FeatureFlagUtils.SETTINGS_GUEST_MODE_UX_CHANGES)
- || !userInfo.isGuest()) {
- // we create a persistent notification only if enabled and only for guests
+ if (!userInfo.isGuest()) {
+ // we create a persistent notification only for guests
return;
}
String contentText;
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 2dadf57..e549a96 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -241,7 +241,6 @@
notifCollection,
notifPipeline,
sysUiState,
- dumpManager,
sysuiMainExecutor));
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java
index 7e4a108..823255c 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java
@@ -113,7 +113,7 @@
}
void setExtraStatusBarItemViews(List<View> views) {
- mSystemStatusViewGroup.removeAllViews();
+ removeAllStatusBarItemViews();
views.forEach(view -> mSystemStatusViewGroup.addView(view));
}
@@ -121,4 +121,8 @@
final View statusIcon = findViewById(resId);
return Objects.requireNonNull(statusIcon);
}
+
+ void removeAllStatusBarItemViews() {
+ mSystemStatusViewGroup.removeAllViews();
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
index 65cfae1..6f50550 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
@@ -192,6 +192,7 @@
mDreamOverlayNotificationCountProvider.ifPresent(
provider -> provider.removeCallback(mNotificationCountCallback));
mStatusBarItemsProvider.removeCallback(mStatusBarItemsProviderCallback);
+ mView.removeAllStatusBarItemViews();
mTouchInsetSession.clear();
mIsAttached = false;
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index 1f356cb..677f0a2 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -154,7 +154,11 @@
public static final ReleasedFlag STATUS_BAR_LETTERBOX_APPEARANCE =
new ReleasedFlag(603, false);
- public static final UnreleasedFlag NEW_STATUS_BAR_PIPELINE = new UnreleasedFlag(604, true);
+ public static final UnreleasedFlag NEW_STATUS_BAR_PIPELINE_BACKEND =
+ new UnreleasedFlag(604, true);
+
+ public static final UnreleasedFlag NEW_STATUS_BAR_PIPELINE_FRONTEND =
+ new UnreleasedFlag(605, true);
/***************************************/
// 700 - dialer/calls
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
index 19c6249..c4e3d4e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
@@ -251,9 +251,21 @@
Utils.getColorAttr(view.context, com.android.internal.R.attr.colorSurface)
view.contentDescription = view.context.getString(viewModel.contentDescriptionResourceId)
- view.setOnClickListener {
+ view.isClickable = viewModel.isClickable
+ if (viewModel.isClickable) {
+ view.setOnClickListener(OnClickListener(viewModel, falsingManager))
+ } else {
+ view.setOnClickListener(null)
+ }
+ }
+
+ private class OnClickListener(
+ private val viewModel: KeyguardQuickAffordanceViewModel,
+ private val falsingManager: FalsingManager,
+ ) : View.OnClickListener {
+ override fun onClick(view: View) {
if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
- return@setOnClickListener
+ return
}
if (viewModel.configKey != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
index 01d5e5c..e3ebac6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
@@ -16,6 +16,7 @@
package com.android.systemui.keyguard.ui.viewmodel
+import androidx.annotation.VisibleForTesting
import com.android.systemui.doze.util.BurnInHelperWrapper
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -37,6 +38,23 @@
private val bottomAreaInteractor: KeyguardBottomAreaInteractor,
private val burnInHelperWrapper: BurnInHelperWrapper,
) {
+ /**
+ * Whether quick affordances are "opaque enough" to be considered visible to and interactive by
+ * the user. If they are not interactive, user input should not be allowed on them.
+ *
+ * Note that there is a margin of error, where we allow very, very slightly transparent views to
+ * be considered "fully opaque" for the purpose of being interactive. This is to accommodate the
+ * error margin of floating point arithmetic.
+ *
+ * A view that is visible but with an alpha of less than our threshold either means it's not
+ * fully done fading in or is fading/faded out. Either way, it should not be
+ * interactive/clickable unless "fully opaque" to avoid issues like in b/241830987.
+ */
+ private val areQuickAffordancesFullyOpaque: Flow<Boolean> =
+ bottomAreaInteractor.alpha
+ .map { alpha -> alpha >= AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD }
+ .distinctUntilChanged()
+
/** An observable for the view-model of the "start button" quick affordance. */
val startButton: Flow<KeyguardQuickAffordanceViewModel> =
button(KeyguardQuickAffordancePosition.BOTTOM_START)
@@ -77,14 +95,19 @@
return combine(
quickAffordanceInteractor.quickAffordance(position),
bottomAreaInteractor.animateDozingTransitions.distinctUntilChanged(),
- ) { model, animateReveal ->
- model.toViewModel(animateReveal)
+ areQuickAffordancesFullyOpaque,
+ ) { model, animateReveal, isFullyOpaque ->
+ model.toViewModel(
+ animateReveal = animateReveal,
+ isClickable = isFullyOpaque,
+ )
}
.distinctUntilChanged()
}
private fun KeyguardQuickAffordanceModel.toViewModel(
animateReveal: Boolean,
+ isClickable: Boolean,
): KeyguardQuickAffordanceViewModel {
return when (this) {
is KeyguardQuickAffordanceModel.Visible ->
@@ -100,8 +123,20 @@
animationController = parameters.animationController,
)
},
+ isClickable = isClickable,
)
is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel()
}
}
+
+ companion object {
+ // We select a value that's less than 1.0 because we want floating point math precision to
+ // not be a factor in determining whether the affordance UI is fully opaque. The number we
+ // choose needs to be close enough 1.0 such that the user can't easily tell the difference
+ // between the UI with an alpha at the threshold and when the alpha is 1.0. At the same
+ // time, we don't want the number to be too close to 1.0 such that there is a chance that we
+ // never treat the affordance UI as "fully opaque" as that would risk making it forever not
+ // clickable.
+ @VisibleForTesting const val AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD = 0.95f
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
index 985ab62..b1de27d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
@@ -31,6 +31,7 @@
val icon: ContainedDrawable = ContainedDrawable.WithResource(0),
@StringRes val contentDescriptionResourceId: Int = 0,
val onClicked: (OnClickedParameters) -> Unit = {},
+ val isClickable: Boolean = false,
) {
data class OnClickedParameters(
val configKey: KClass<out KeyguardQuickAffordanceConfig>,
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
index e360d10..ee59561 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -16,6 +16,7 @@
package com.android.systemui.media.dialog;
+import android.annotation.DrawableRes;
import android.content.res.ColorStateList;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
@@ -42,9 +43,6 @@
private static final String TAG = "MediaOutputAdapter";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
- private ViewGroup mConnectedItem;
- private boolean mIncludeDynamicGroup;
-
public MediaOutputAdapter(MediaOutputController controller) {
super(controller);
setHasStableIds(true);
@@ -102,141 +100,90 @@
void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin, int position) {
super.onBind(device, topMargin, bottomMargin, position);
boolean isMutingExpectedDeviceExist = mController.hasMutingExpectedDevice();
- final boolean currentlyConnected = !mIncludeDynamicGroup
- && isCurrentlyConnected(device);
+ final boolean currentlyConnected = isCurrentlyConnected(device);
boolean isCurrentSeekbarInvisible = mSeekBar.getVisibility() == View.GONE;
- if (currentlyConnected) {
- mConnectedItem = mContainerLayout;
- }
- mCheckBox.setVisibility(View.GONE);
- mStatusIcon.setVisibility(View.GONE);
- mEndTouchArea.setVisibility(View.GONE);
- mEndTouchArea.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
- mContainerLayout.setOnClickListener(null);
- mContainerLayout.setContentDescription(null);
- mTitleText.setTextColor(mController.getColorItemContent());
- mSubTitleText.setTextColor(mController.getColorItemContent());
- mTwoLineTitleText.setTextColor(mController.getColorItemContent());
- mSeekBar.getProgressDrawable().setColorFilter(
- new PorterDuffColorFilter(mController.getColorSeekbarProgress(),
- PorterDuff.Mode.SRC_IN));
if (mCurrentActivePosition == position) {
mCurrentActivePosition = -1;
}
- if (mController.isTransferring()) {
+ if (mController.isAnyDeviceTransferring()) {
if (device.getState() == MediaDeviceState.STATE_CONNECTING
&& !mController.hasAdjustVolumeUserRestriction()) {
setUpDeviceIcon(device);
- mProgressBar.getIndeterminateDrawable().setColorFilter(
- new PorterDuffColorFilter(
- mController.getColorItemContent(),
- PorterDuff.Mode.SRC_IN));
- setSingleLineLayout(getItemTitle(device), true /* bFocused */,
- false /* showSeekBar*/,
- true /* showProgressBar */, false /* showStatus */);
+ updateProgressBarColor();
+ setSingleLineLayout(getItemTitle(device), false /* showSeekBar*/,
+ true /* showProgressBar */, false /* showCheckBox */,
+ false /* showEndTouchArea */);
} else {
setUpDeviceIcon(device);
- setSingleLineLayout(getItemTitle(device), false /* bFocused */);
+ setSingleLineLayout(getItemTitle(device));
}
} else {
// Set different layout for each device
if (device.isMutingExpectedDevice()
&& !mController.isCurrentConnectedDeviceRemote()) {
- mTitleIcon.setImageDrawable(
- mContext.getDrawable(R.drawable.media_output_icon_volume));
- mTitleIcon.setColorFilter(mController.getColorItemContent());
- mTitleText.setTextColor(mController.getColorItemContent());
- setSingleLineLayout(getItemTitle(device), true /* bFocused */,
- false /* showSeekBar */,
- false /* showProgressBar */, false /* showStatus */);
+ updateTitleIcon(R.drawable.media_output_icon_volume,
+ mController.getColorItemContent());
initMutingExpectedDevice();
mCurrentActivePosition = position;
- mContainerLayout.setOnClickListener(v -> onItemClick(v, device));
+ updateContainerClickListener(v -> onItemClick(v, device));
+ setSingleLineLayout(getItemTitle(device));
} else if (device.getState() == MediaDeviceState.STATE_CONNECTING_FAILED) {
setUpDeviceIcon(device);
- mStatusIcon.setImageDrawable(
- mContext.getDrawable(R.drawable.media_output_status_failed));
- mStatusIcon.setColorFilter(mController.getColorItemContent());
- setTwoLineLayout(device, false /* bFocused */,
- false /* showSeekBar */, false /* showProgressBar */,
- true /* showSubtitle */, true /* showStatus */);
+ updateConnectionFailedStatusIcon();
mSubTitleText.setText(R.string.media_output_dialog_connect_failed);
- mContainerLayout.setOnClickListener(v -> onItemClick(v, device));
+ updateContainerClickListener(v -> onItemClick(v, device));
+ setTwoLineLayout(device, false /* bFocused */, false /* showSeekBar */,
+ false /* showProgressBar */, true /* showSubtitle */,
+ true /* showStatus */);
} else if (device.getState() == MediaDeviceState.STATE_GROUPING) {
setUpDeviceIcon(device);
- mProgressBar.getIndeterminateDrawable().setColorFilter(
- new PorterDuffColorFilter(
- mController.getColorItemContent(),
- PorterDuff.Mode.SRC_IN));
- setSingleLineLayout(getItemTitle(device), true /* bFocused */,
- false /* showSeekBar*/,
- true /* showProgressBar */, false /* showStatus */);
+ updateProgressBarColor();
+ setSingleLineLayout(getItemTitle(device), false /* showSeekBar*/,
+ true /* showProgressBar */, false /* showCheckBox */,
+ false /* showEndTouchArea */);
} else if (mController.getSelectedMediaDevice().size() > 1
&& isDeviceIncluded(mController.getSelectedMediaDevice(), device)) {
boolean isDeviceDeselectable = isDeviceIncluded(
mController.getDeselectableMediaDevice(), device);
- mTitleText.setTextColor(mController.getColorItemContent());
- mTitleIcon.setImageDrawable(
- mContext.getDrawable(R.drawable.media_output_icon_volume));
- mTitleIcon.setColorFilter(mController.getColorItemContent());
- setSingleLineLayout(getItemTitle(device), true /* bFocused */,
- true /* showSeekBar */,
- false /* showProgressBar */, false /* showStatus */);
+ updateTitleIcon(R.drawable.media_output_icon_volume,
+ mController.getColorItemContent());
+ updateGroupableCheckBox(true, isDeviceDeselectable, device);
+ updateEndClickArea(device, isDeviceDeselectable);
setUpContentDescriptionForView(mContainerLayout, false, device);
- mCheckBox.setOnCheckedChangeListener(null);
- mCheckBox.setVisibility(View.VISIBLE);
- mCheckBox.setChecked(true);
- mCheckBox.setOnCheckedChangeListener(isDeviceDeselectable
- ? (buttonView, isChecked) -> onGroupActionTriggered(false, device)
- : null);
- mCheckBox.setEnabled(isDeviceDeselectable);
- setCheckBoxColor(mCheckBox, mController.getColorItemContent());
+ setSingleLineLayout(getItemTitle(device), true /* showSeekBar */,
+ false /* showProgressBar */, true /* showCheckBox */,
+ true /* showEndTouchArea */);
initSeekbar(device, isCurrentSeekbarInvisible);
- mEndTouchArea.setVisibility(View.VISIBLE);
- mEndTouchArea.setOnClickListener(null);
- mEndTouchArea.setOnClickListener(
- isDeviceDeselectable ? (v) -> mCheckBox.performClick() : null);
- mEndTouchArea.setImportantForAccessibility(
- View.IMPORTANT_FOR_ACCESSIBILITY_YES);
- setUpContentDescriptionForView(mEndTouchArea, true, device);
} else if (!mController.hasAdjustVolumeUserRestriction()
&& currentlyConnected) {
if (isMutingExpectedDeviceExist
&& !mController.isCurrentConnectedDeviceRemote()) {
// mark as disconnected and set special click listener
setUpDeviceIcon(device);
- setSingleLineLayout(getItemTitle(device), false /* bFocused */);
- mContainerLayout.setOnClickListener(v -> cancelMuteAwaitConnection());
+ updateContainerClickListener(v -> cancelMuteAwaitConnection());
+ setSingleLineLayout(getItemTitle(device));
} else {
- mTitleIcon.setImageDrawable(
- mContext.getDrawable(R.drawable.media_output_icon_volume));
- mTitleIcon.setColorFilter(mController.getColorItemContent());
- mTitleText.setTextColor(mController.getColorItemContent());
- setSingleLineLayout(getItemTitle(device), true /* bFocused */,
- true /* showSeekBar */,
- false /* showProgressBar */, false /* showStatus */);
- initSeekbar(device, isCurrentSeekbarInvisible);
+ updateTitleIcon(R.drawable.media_output_icon_volume,
+ mController.getColorItemContent());
setUpContentDescriptionForView(mContainerLayout, false, device);
mCurrentActivePosition = position;
+ setSingleLineLayout(getItemTitle(device), true /* showSeekBar */,
+ false /* showProgressBar */, false /* showCheckBox */,
+ false /* showEndTouchArea */);
+ initSeekbar(device, isCurrentSeekbarInvisible);
}
} else if (isDeviceIncluded(mController.getSelectableMediaDevice(), device)) {
setUpDeviceIcon(device);
- mCheckBox.setOnCheckedChangeListener(null);
- mCheckBox.setVisibility(View.VISIBLE);
- mCheckBox.setChecked(false);
- mCheckBox.setOnCheckedChangeListener(
- (buttonView, isChecked) -> onGroupActionTriggered(true, device));
- mEndTouchArea.setVisibility(View.VISIBLE);
- mContainerLayout.setOnClickListener(v -> onGroupActionTriggered(true, device));
- setCheckBoxColor(mCheckBox, mController.getColorItemContent());
- setSingleLineLayout(getItemTitle(device), false /* bFocused */,
- false /* showSeekBar */,
- false /* showProgressBar */, false /* showStatus */);
+ updateGroupableCheckBox(false, true, device);
+ updateContainerClickListener(v -> onGroupActionTriggered(true, device));
+ setSingleLineLayout(getItemTitle(device), false /* showSeekBar */,
+ false /* showProgressBar */, true /* showCheckBox */,
+ true /* showEndTouchArea */);
} else {
setUpDeviceIcon(device);
- setSingleLineLayout(getItemTitle(device), false /* bFocused */);
- mContainerLayout.setOnClickListener(v -> onItemClick(v, device));
+ setSingleLineLayout(getItemTitle(device));
+ updateContainerClickListener(v -> onItemClick(v, device));
}
}
}
@@ -248,15 +195,56 @@
ColorStateList(states, colors));
}
+ private void updateConnectionFailedStatusIcon() {
+ mStatusIcon.setImageDrawable(
+ mContext.getDrawable(R.drawable.media_output_status_failed));
+ mStatusIcon.setColorFilter(mController.getColorItemContent());
+ }
+
+ private void updateProgressBarColor() {
+ mProgressBar.getIndeterminateDrawable().setColorFilter(
+ new PorterDuffColorFilter(
+ mController.getColorItemContent(),
+ PorterDuff.Mode.SRC_IN));
+ }
+
+ public void updateEndClickArea(MediaDevice device, boolean isDeviceDeselectable) {
+ mEndTouchArea.setOnClickListener(null);
+ mEndTouchArea.setOnClickListener(
+ isDeviceDeselectable ? (v) -> mCheckBox.performClick() : null);
+ mEndTouchArea.setImportantForAccessibility(
+ View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ setUpContentDescriptionForView(mEndTouchArea, true, device);
+ }
+
+ private void updateGroupableCheckBox(boolean isSelected, boolean isGroupable,
+ MediaDevice device) {
+ mCheckBox.setOnCheckedChangeListener(null);
+ mCheckBox.setChecked(isSelected);
+ mCheckBox.setOnCheckedChangeListener(
+ isGroupable ? (buttonView, isChecked) -> onGroupActionTriggered(!isSelected,
+ device) : null);
+ mCheckBox.setEnabled(isGroupable);
+ setCheckBoxColor(mCheckBox, mController.getColorItemContent());
+ }
+
+ private void updateTitleIcon(@DrawableRes int id, int color) {
+ mTitleIcon.setImageDrawable(mContext.getDrawable(id));
+ mTitleIcon.setColorFilter(color);
+ }
+
+ private void updateContainerClickListener(View.OnClickListener listener) {
+ mContainerLayout.setOnClickListener(listener);
+ }
+
@Override
void onBind(int customizedItem, boolean topMargin, boolean bottomMargin) {
if (customizedItem == CUSTOMIZED_ITEM_PAIR_NEW) {
mTitleText.setTextColor(mController.getColorItemContent());
mCheckBox.setVisibility(View.GONE);
- setSingleLineLayout(mContext.getText(R.string.media_output_dialog_pairing_new),
- false /* bFocused */);
- final Drawable d = mContext.getDrawable(R.drawable.ic_add);
- mTitleIcon.setImageDrawable(d);
+ setSingleLineLayout(mContext.getText(R.string.media_output_dialog_pairing_new));
+ final Drawable addDrawable = mContext.getDrawable(R.drawable.ic_add);
+ mTitleIcon.setImageDrawable(addDrawable);
mTitleIcon.setColorFilter(mController.getColorItemContent());
mContainerLayout.setOnClickListener(mController::launchBluetoothPairing);
}
@@ -273,7 +261,7 @@
}
private void onItemClick(View view, MediaDevice device) {
- if (mController.isTransferring()) {
+ if (mController.isAnyDeviceTransferring()) {
return;
}
if (isCurrentlyConnected(device)) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index 43b0287..3f7b226 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -63,8 +63,6 @@
protected final MediaOutputController mController;
- private int mMargin;
-
Context mContext;
View mHolderView;
boolean mIsDragging;
@@ -82,8 +80,6 @@
public MediaDeviceBaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup,
int viewType) {
mContext = viewGroup.getContext();
- mMargin = mContext.getResources().getDimensionPixelSize(
- R.dimen.media_output_dialog_list_margin);
mHolderView = LayoutInflater.from(mContext).inflate(R.layout.media_output_list_item,
viewGroup, false);
@@ -168,16 +164,28 @@
void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin, int position) {
mDeviceId = device.getId();
+ mCheckBox.setVisibility(View.GONE);
+ mStatusIcon.setVisibility(View.GONE);
+ mEndTouchArea.setVisibility(View.GONE);
+ mEndTouchArea.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+ mContainerLayout.setOnClickListener(null);
+ mContainerLayout.setContentDescription(null);
+ mTitleText.setTextColor(mController.getColorItemContent());
+ mSubTitleText.setTextColor(mController.getColorItemContent());
+ mTwoLineTitleText.setTextColor(mController.getColorItemContent());
+ mSeekBar.getProgressDrawable().setColorFilter(
+ new PorterDuffColorFilter(mController.getColorSeekbarProgress(),
+ PorterDuff.Mode.SRC_IN));
}
abstract void onBind(int customizedItem, boolean topMargin, boolean bottomMargin);
- void setSingleLineLayout(CharSequence title, boolean bFocused) {
- setSingleLineLayout(title, bFocused, false, false, false);
+ void setSingleLineLayout(CharSequence title) {
+ setSingleLineLayout(title, false, false, false, false);
}
- void setSingleLineLayout(CharSequence title, boolean bFocused, boolean showSeekBar,
- boolean showProgressBar, boolean showStatus) {
+ void setSingleLineLayout(CharSequence title, boolean showSeekBar,
+ boolean showProgressBar, boolean showCheckBox, boolean showEndTouchArea) {
mTwoLineLayout.setVisibility(View.GONE);
boolean isActive = showSeekBar || showProgressBar;
if (!mCornerAnimator.isRunning()) {
@@ -188,10 +196,6 @@
.mutate() : mContext.getDrawable(
R.drawable.media_output_item_background)
.mutate();
- backgroundDrawable.setColorFilter(new PorterDuffColorFilter(
- isActive ? mController.getColorConnectedItemBackground()
- : mController.getColorItemBackground(),
- PorterDuff.Mode.SRC_IN));
mItemLayout.setBackground(backgroundDrawable);
if (showSeekBar) {
final ClipDrawable clipDrawable =
@@ -201,27 +205,21 @@
(GradientDrawable) clipDrawable.getDrawable();
progressDrawable.setCornerRadius(mController.getActiveRadius());
}
- } else {
- mItemLayout.getBackground().setColorFilter(new PorterDuffColorFilter(
- isActive ? mController.getColorConnectedItemBackground()
- : mController.getColorItemBackground(),
- PorterDuff.Mode.SRC_IN));
}
+ mItemLayout.getBackground().setColorFilter(new PorterDuffColorFilter(
+ isActive ? mController.getColorConnectedItemBackground()
+ : mController.getColorItemBackground(),
+ PorterDuff.Mode.SRC_IN));
mProgressBar.setVisibility(showProgressBar ? View.VISIBLE : View.GONE);
mSeekBar.setAlpha(1);
mSeekBar.setVisibility(showSeekBar ? View.VISIBLE : View.GONE);
if (!showSeekBar) {
mSeekBar.resetVolume();
}
- mStatusIcon.setVisibility(showStatus ? View.VISIBLE : View.GONE);
mTitleText.setText(title);
mTitleText.setVisibility(View.VISIBLE);
- }
-
- void setTwoLineLayout(MediaDevice device, boolean bFocused, boolean showSeekBar,
- boolean showProgressBar, boolean showSubtitle) {
- setTwoLineLayout(device, null, bFocused, showSeekBar, showProgressBar, showSubtitle,
- false);
+ mCheckBox.setVisibility(showCheckBox ? View.VISIBLE : View.GONE);
+ mEndTouchArea.setVisibility(showEndTouchArea ? View.VISIBLE : View.GONE);
}
void setTwoLineLayout(MediaDevice device, boolean bFocused, boolean showSeekBar,
@@ -230,12 +228,6 @@
showStatus);
}
- void setTwoLineLayout(CharSequence title, boolean bFocused, boolean showSeekBar,
- boolean showProgressBar, boolean showSubtitle) {
- setTwoLineLayout(null, title, bFocused, showSeekBar, showProgressBar, showSubtitle,
- false);
- }
-
private void setTwoLineLayout(MediaDevice device, CharSequence title, boolean bFocused,
boolean showSeekBar, boolean showProgressBar, boolean showSubtitle,
boolean showStatus) {
@@ -254,20 +246,11 @@
mProgressBar.setVisibility(showProgressBar ? View.VISIBLE : View.GONE);
mSubTitleText.setVisibility(showSubtitle ? View.VISIBLE : View.GONE);
mTwoLineTitleText.setTranslationY(0);
- if (device == null) {
- mTwoLineTitleText.setText(title);
- } else {
- mTwoLineTitleText.setText(getItemTitle(device));
- }
-
- if (bFocused) {
- mTwoLineTitleText.setTypeface(Typeface.create(mContext.getString(
- com.android.internal.R.string.config_headlineFontFamilyMedium),
- Typeface.NORMAL));
- } else {
- mTwoLineTitleText.setTypeface(Typeface.create(mContext.getString(
- com.android.internal.R.string.config_headlineFontFamily), Typeface.NORMAL));
- }
+ mTwoLineTitleText.setText(device == null ? title : getItemTitle(device));
+ mTwoLineTitleText.setTypeface(Typeface.create(mContext.getString(
+ bFocused ? com.android.internal.R.string.config_headlineFontFamilyMedium
+ : com.android.internal.R.string.config_headlineFontFamily),
+ Typeface.NORMAL));
}
void initSeekbar(MediaDevice device, boolean isCurrentSeekbarInvisible) {
@@ -327,35 +310,6 @@
mItemLayout.setBackground(backgroundDrawable);
}
- void initSessionSeekbar() {
- disableSeekBar();
- mSeekBar.setMax(mController.getSessionVolumeMax());
- mSeekBar.setMin(0);
- final int currentVolume = mController.getSessionVolume();
- if (mSeekBar.getProgress() != currentVolume) {
- mSeekBar.setProgress(currentVolume, true);
- }
- mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
- @Override
- public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
- if (!fromUser) {
- return;
- }
- mController.adjustSessionVolume(progress);
- }
-
- @Override
- public void onStartTrackingTouch(SeekBar seekBar) {
- mIsDragging = true;
- }
-
- @Override
- public void onStopTrackingTouch(SeekBar seekBar) {
- mIsDragging = false;
- }
- });
- }
-
private void animateCornerAndVolume(int fromProgress, int toProgress) {
final GradientDrawable layoutBackgroundDrawable =
(GradientDrawable) mItemLayout.getBackground();
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index f7d80e0..96817c9 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -411,7 +411,7 @@
device.getId());
boolean isSelectedDeviceInGroup = getSelectedMediaDevice().size() > 1
&& getSelectedMediaDevice().contains(device);
- return (!hasAdjustVolumeUserRestriction() && isConnected && !isTransferring())
+ return (!hasAdjustVolumeUserRestriction() && isConnected && !isAnyDeviceTransferring())
|| isSelectedDeviceInGroup;
}
@@ -708,7 +708,7 @@
UserHandle.of(UserHandle.myUserId()));
}
- boolean isTransferring() {
+ boolean isAnyDeviceTransferring() {
synchronized (mMediaDevicesLock) {
for (MediaDevice device : mMediaDevices) {
if (device.getState() == LocalMediaManager.MediaDeviceState.STATE_CONNECTING) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt
index 2278938..3a0ac1b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt
@@ -226,7 +226,7 @@
appIconView.contentDescription = appNameOverride ?: iconInfo.iconName
appIconView.setImageDrawable(appIconDrawableOverride ?: iconInfo.icon)
- return appIconView.contentDescription.toString()
+ return appIconView.contentDescription
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
index 196ea22..00a22f2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
@@ -141,12 +141,13 @@
override fun updateChipView(newChipInfo: ChipReceiverInfo, currentChipView: ViewGroup) {
super.updateChipView(newChipInfo, currentChipView)
- setIcon(
+ val iconName = setIcon(
currentChipView,
newChipInfo.routeInfo.clientPackageName,
newChipInfo.appIconDrawableOverride,
newChipInfo.appNameOverride
)
+ currentChipView.contentDescription = iconName
}
override fun animateChipIn(chipView: ViewGroup) {
@@ -159,6 +160,8 @@
.alpha(1f)
.setDuration(5.frames)
.start()
+ // Using withEndAction{} doesn't apply a11y focus when screen is unlocked.
+ appIconView.postOnAnimation { chipView.requestAccessibilityFocus() }
startRipple(chipView.requireViewById(R.id.ripple))
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java
index eeb1010..2a6cf66 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java
@@ -80,7 +80,8 @@
FeatureFlags featureFlags,
VariableDateViewController.Factory variableDateViewControllerFactory,
BatteryMeterViewController batteryMeterViewController,
- StatusBarContentInsetsProvider statusBarContentInsetsProvider) {
+ StatusBarContentInsetsProvider statusBarContentInsetsProvider,
+ StatusBarIconController.TintedIconManager.Factory tintedIconManagerFactory) {
super(view);
mPrivacyIconsController = headerPrivacyIconsController;
mStatusBarIconController = statusBarIconController;
@@ -103,7 +104,7 @@
mView.requireViewById(R.id.date_clock)
);
- mIconManager = new StatusBarIconController.TintedIconManager(mIconContainer, featureFlags);
+ mIconManager = tintedIconManagerFactory.create(mIconContainer);
mDemoModeReceiver = new ClockDemoModeReceiver(mClockView);
mColorExtractor = colorExtractor;
mOnColorsChangedListener = (extractor, which) -> {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
index a8993bc..8b37aab 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
@@ -70,7 +70,7 @@
public class TakeScreenshotService extends Service {
private static final String TAG = logTag(TakeScreenshotService.class);
- private ScreenshotController mScreenshot;
+ private final ScreenshotController mScreenshot;
private final UserManager mUserManager;
private final DevicePolicyManager mDevicePolicyManager;
@@ -152,10 +152,7 @@
if (DEBUG_SERVICE) {
Log.d(TAG, "onUnbind");
}
- if (mScreenshot != null) {
- mScreenshot.removeWindow();
- mScreenshot = null;
- }
+ mScreenshot.removeWindow();
unregisterReceiver(mCloseSystemDialogs);
return false;
}
@@ -163,10 +160,7 @@
@Override
public void onDestroy() {
super.onDestroy();
- if (mScreenshot != null) {
- mScreenshot.onDestroy();
- mScreenshot = null;
- }
+ mScreenshot.onDestroy();
if (DEBUG_SERVICE) {
Log.d(TAG, "onDestroy");
}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt
index 5e908d9..1558ac5 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt
@@ -76,6 +76,6 @@
* Notifies that the current user's profiles have changed.
*/
@JvmDefault
- fun onProfilesChanged(profiles: List<UserInfo>) {}
+ fun onProfilesChanged(profiles: List<@JvmSuppressWildcards UserInfo>) {}
}
}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
index 0f9ac36..fab70fc 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
@@ -77,6 +77,7 @@
class LargeScreenShadeHeaderController @Inject constructor(
@Named(LARGE_SCREEN_SHADE_HEADER) private val header: View,
private val statusBarIconController: StatusBarIconController,
+ private val tintedIconManagerFactory: StatusBarIconController.TintedIconManager.Factory,
private val privacyIconsController: HeaderPrivacyIconsController,
private val insetsProvider: StatusBarContentInsetsProvider,
private val configurationController: ConfigurationController,
@@ -259,7 +260,7 @@
batteryMeterViewController.ignoreTunerUpdates()
batteryIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
- iconManager = StatusBarIconController.TintedIconManager(iconContainer, featureFlags)
+ iconManager = tintedIconManagerFactory.create(iconContainer)
iconManager.setTint(
Utils.getColorAttrDefaultColor(header.context, android.R.attr.textColorPrimary)
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarWifiView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarWifiView.kt
new file mode 100644
index 0000000..4d53064
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarWifiView.kt
@@ -0,0 +1,34 @@
+/*
+ * 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
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.FrameLayout
+
+/**
+ * A temporary base class that's shared between our old status bar wifi view implementation
+ * ([StatusBarWifiView]) and our new status bar wifi view implementation
+ * ([ModernStatusBarWifiView]).
+ *
+ * Once our refactor is over, we should be able to delete this go-between class and the old view
+ * class.
+ */
+abstract class BaseStatusBarWifiView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttrs: Int = 0,
+) : FrameLayout(context, attrs, defStyleAttrs), StatusIconDisplayable
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java
index a6986d7..5aee62e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java
@@ -28,7 +28,6 @@
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
-import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
@@ -41,8 +40,7 @@
/**
* Start small: StatusBarWifiView will be able to layout from a WifiIconState
*/
-public class StatusBarWifiView extends FrameLayout implements DarkReceiver,
- StatusIconDisplayable {
+public class StatusBarWifiView extends BaseStatusBarWifiView implements DarkReceiver {
private static final String TAG = "StatusBarWifiView";
/// Used to show etc dots
@@ -80,11 +78,6 @@
super(context, attrs, defStyleAttr);
}
- public StatusBarWifiView(Context context, AttributeSet attrs, int defStyleAttr,
- int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
-
public void setSlot(String slot) {
mSlot = slot;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiIcons.java
index 3c449ad..7097568 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiIcons.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiIcons.java
@@ -23,7 +23,7 @@
/** */
public class WifiIcons {
- static final int[] WIFI_FULL_ICONS = {
+ public static final int[] WIFI_FULL_ICONS = {
com.android.internal.R.drawable.ic_wifi_signal_0,
com.android.internal.R.drawable.ic_wifi_signal_1,
com.android.internal.R.drawable.ic_wifi_signal_2,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index b0a5adb..6f06759 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -4065,7 +4065,7 @@
final PendingIntent intent, @Nullable final Runnable intentSentUiThreadCallback,
@Nullable ActivityLaunchAnimator.Controller animationController) {
final boolean willLaunchResolverActivity = intent.isActivity()
- && mActivityIntentHelper.wouldLaunchResolverActivity(intent.getIntent(),
+ && mActivityIntentHelper.wouldPendingLaunchResolverActivity(intent,
mLockscreenUserManager.getCurrentUserId());
boolean animate = !willLaunchResolverActivity
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
index fc8e7d5..deb6150 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
@@ -225,6 +225,7 @@
public void addDemoWifiView(WifiIconState state) {
Log.d(TAG, "addDemoWifiView: ");
+ // TODO(b/238425913): Migrate this view to {@code ModernStatusBarWifiView}.
StatusBarWifiView view = StatusBarWifiView.fromContext(mContext, state.slot);
int viewIndex = getChildCount();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
index 30b640b..ed186ab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -40,6 +40,7 @@
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.plugins.DarkIconDispatcher;
import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
+import com.android.systemui.statusbar.BaseStatusBarWifiView;
import com.android.systemui.statusbar.StatusBarIconView;
import com.android.systemui.statusbar.StatusBarMobileView;
import com.android.systemui.statusbar.StatusBarWifiView;
@@ -47,12 +48,16 @@
import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState;
import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState;
import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState;
+import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
+import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView;
+import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel;
import com.android.systemui.util.Assert;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
+import javax.inject.Provider;
public interface StatusBarIconController {
@@ -128,8 +133,12 @@
private final DarkIconDispatcher mDarkIconDispatcher;
private int mIconHPadding;
- public DarkIconManager(LinearLayout linearLayout, FeatureFlags featureFlags) {
- super(linearLayout, featureFlags);
+ public DarkIconManager(
+ LinearLayout linearLayout,
+ FeatureFlags featureFlags,
+ StatusBarPipelineFlags statusBarPipelineFlags,
+ Provider<WifiViewModel> wifiViewModelProvider) {
+ super(linearLayout, featureFlags, statusBarPipelineFlags, wifiViewModelProvider);
mIconHPadding = mContext.getResources().getDimensionPixelSize(
R.dimen.status_bar_icon_padding);
mDarkIconDispatcher = Dependency.get(DarkIconDispatcher.class);
@@ -183,14 +192,40 @@
mDarkIconDispatcher.removeDarkReceiver(mDemoStatusIcons);
super.exitDemoMode();
}
+
+ @SysUISingleton
+ public static class Factory {
+ private final FeatureFlags mFeatureFlags;
+ private final StatusBarPipelineFlags mStatusBarPipelineFlags;
+ private final Provider<WifiViewModel> mWifiViewModelProvider;
+
+ @Inject
+ public Factory(
+ FeatureFlags featureFlags,
+ StatusBarPipelineFlags statusBarPipelineFlags,
+ Provider<WifiViewModel> wifiViewModelProvider) {
+ mFeatureFlags = featureFlags;
+ mStatusBarPipelineFlags = statusBarPipelineFlags;
+ mWifiViewModelProvider = wifiViewModelProvider;
+ }
+
+ public DarkIconManager create(LinearLayout group) {
+ return new DarkIconManager(
+ group, mFeatureFlags, mStatusBarPipelineFlags, mWifiViewModelProvider);
+ }
+ }
}
/** */
class TintedIconManager extends IconManager {
private int mColor;
- public TintedIconManager(ViewGroup group, FeatureFlags featureFlags) {
- super(group, featureFlags);
+ public TintedIconManager(
+ ViewGroup group,
+ FeatureFlags featureFlags,
+ StatusBarPipelineFlags statusBarPipelineFlags,
+ Provider<WifiViewModel> wifiViewModelProvider) {
+ super(group, featureFlags, statusBarPipelineFlags, wifiViewModelProvider);
}
@Override
@@ -223,14 +258,22 @@
@SysUISingleton
public static class Factory {
private final FeatureFlags mFeatureFlags;
+ private final StatusBarPipelineFlags mStatusBarPipelineFlags;
+ private final Provider<WifiViewModel> mWifiViewModelProvider;
@Inject
- public Factory(FeatureFlags featureFlags) {
+ public Factory(
+ FeatureFlags featureFlags,
+ StatusBarPipelineFlags statusBarPipelineFlags,
+ Provider<WifiViewModel> wifiViewModelProvider) {
mFeatureFlags = featureFlags;
+ mStatusBarPipelineFlags = statusBarPipelineFlags;
+ mWifiViewModelProvider = wifiViewModelProvider;
}
public TintedIconManager create(ViewGroup group) {
- return new TintedIconManager(group, mFeatureFlags);
+ return new TintedIconManager(
+ group, mFeatureFlags, mStatusBarPipelineFlags, mWifiViewModelProvider);
}
}
}
@@ -239,8 +282,10 @@
* Turns info from StatusBarIconController into ImageViews in a ViewGroup.
*/
class IconManager implements DemoModeCommandReceiver {
- private final FeatureFlags mFeatureFlags;
protected final ViewGroup mGroup;
+ private final FeatureFlags mFeatureFlags;
+ private final StatusBarPipelineFlags mStatusBarPipelineFlags;
+ private final Provider<WifiViewModel> mWifiViewModelProvider;
protected final Context mContext;
protected final int mIconSize;
// Whether or not these icons show up in dumpsys
@@ -254,9 +299,15 @@
protected ArrayList<String> mBlockList = new ArrayList<>();
- public IconManager(ViewGroup group, FeatureFlags featureFlags) {
- mFeatureFlags = featureFlags;
+ public IconManager(
+ ViewGroup group,
+ FeatureFlags featureFlags,
+ StatusBarPipelineFlags statusBarPipelineFlags,
+ Provider<WifiViewModel> wifiViewModelProvider) {
mGroup = group;
+ mFeatureFlags = featureFlags;
+ mStatusBarPipelineFlags = statusBarPipelineFlags;
+ mWifiViewModelProvider = wifiViewModelProvider;
mContext = group.getContext();
mIconSize = mContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.status_bar_icon_size);
@@ -308,7 +359,7 @@
return addIcon(index, slot, blocked, holder.getIcon());
case TYPE_WIFI:
- return addSignalIcon(index, slot, holder.getWifiState());
+ return addWifiIcon(index, slot, holder.getWifiState());
case TYPE_MOBILE:
return addMobileIcon(index, slot, holder.getMobileState());
@@ -327,9 +378,17 @@
}
@VisibleForTesting
- protected StatusBarWifiView addSignalIcon(int index, String slot, WifiIconState state) {
- StatusBarWifiView view = onCreateStatusBarWifiView(slot);
- view.applyWifiState(state);
+ protected StatusIconDisplayable addWifiIcon(int index, String slot, WifiIconState state) {
+ final BaseStatusBarWifiView view;
+ if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+ view = onCreateModernStatusBarWifiView(slot);
+ // When [ModernStatusBarWifiView] is created, it will automatically apply the
+ // correct view state so we don't need to call applyWifiState.
+ } else {
+ StatusBarWifiView wifiView = onCreateStatusBarWifiView(slot);
+ wifiView.applyWifiState(state);
+ view = wifiView;
+ }
mGroup.addView(view, index, onCreateLayoutParams());
if (mIsInDemoMode) {
@@ -359,6 +418,11 @@
return view;
}
+ private ModernStatusBarWifiView onCreateModernStatusBarWifiView(String slot) {
+ return ModernStatusBarWifiView.constructAndBind(
+ mContext, slot, mWifiViewModelProvider.get());
+ }
+
private StatusBarMobileView onCreateStatusBarMobileView(String slot) {
StatusBarMobileView view = StatusBarMobileView.fromContext(mContext, slot);
return view;
@@ -415,9 +479,8 @@
onSetIcon(viewIndex, holder.getIcon());
return;
case TYPE_WIFI:
- onSetSignalIcon(viewIndex, holder.getWifiState());
+ onSetWifiIcon(viewIndex, holder.getWifiState());
return;
-
case TYPE_MOBILE:
onSetMobileIcon(viewIndex, holder.getMobileState());
default:
@@ -425,10 +488,16 @@
}
}
- public void onSetSignalIcon(int viewIndex, WifiIconState state) {
- StatusBarWifiView wifiView = (StatusBarWifiView) mGroup.getChildAt(viewIndex);
- if (wifiView != null) {
- wifiView.applyWifiState(state);
+ public void onSetWifiIcon(int viewIndex, WifiIconState state) {
+ View view = mGroup.getChildAt(viewIndex);
+ if (view instanceof StatusBarWifiView) {
+ ((StatusBarWifiView) view).applyWifiState(state);
+ } else if (view instanceof ModernStatusBarWifiView) {
+ // ModernStatusBarWifiView will automatically apply state based on its callbacks, so
+ // we don't need to call applyWifiState.
+ } else {
+ throw new IllegalStateException("View at " + viewIndex + " must be of type "
+ + "StatusBarWifiView or ModernStatusBarWifiView");
}
if (mIsInDemoMode) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
index 374f091..5cd2ba1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -223,12 +223,12 @@
boolean isActivityIntent = intent != null && intent.isActivity() && !isBubble;
final boolean willLaunchResolverActivity = isActivityIntent
- && mActivityIntentHelper.wouldLaunchResolverActivity(intent.getIntent(),
+ && mActivityIntentHelper.wouldPendingLaunchResolverActivity(intent,
mLockscreenUserManager.getCurrentUserId());
final boolean animate = !willLaunchResolverActivity
&& mCentralSurfaces.shouldAnimateLaunch(isActivityIntent);
boolean showOverLockscreen = mKeyguardStateController.isShowing() && intent != null
- && mActivityIntentHelper.wouldShowOverLockscreen(intent.getIntent(),
+ && mActivityIntentHelper.wouldPendingShowOverLockscreen(intent,
mLockscreenUserManager.getCurrentUserId());
ActivityStarter.OnDismissAction postKeyguardAction = new ActivityStarter.OnDismissAction() {
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
index 40b9a15..70af77e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
@@ -259,8 +259,9 @@
final boolean isActivity = pendingIntent.isActivity();
if (isActivity || appRequestedAuth) {
mActionClickLogger.logWaitingToCloseKeyguard(pendingIntent);
- final boolean afterKeyguardGone = mActivityIntentHelper.wouldLaunchResolverActivity(
- pendingIntent.getIntent(), mLockscreenUserManager.getCurrentUserId());
+ final boolean afterKeyguardGone = mActivityIntentHelper
+ .wouldPendingLaunchResolverActivity(pendingIntent,
+ mLockscreenUserManager.getCurrentUserId());
mActivityStarter.dismissKeyguardThenExecute(() -> {
mActionClickLogger.logKeyguardGone(pendingIntent);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
index 200f45f..fb5b096 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
@@ -286,6 +286,7 @@
PanelExpansionStateManager panelExpansionStateManager,
FeatureFlags featureFlags,
StatusBarIconController statusBarIconController,
+ StatusBarIconController.DarkIconManager.Factory darkIconManagerFactory,
StatusBarHideIconsForBouncerManager statusBarHideIconsForBouncerManager,
KeyguardStateController keyguardStateController,
NotificationPanelViewController notificationPanelViewController,
@@ -306,6 +307,7 @@
panelExpansionStateManager,
featureFlags,
statusBarIconController,
+ darkIconManagerFactory,
statusBarHideIconsForBouncerManager,
keyguardStateController,
notificationPanelViewController,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index f61b488..5fd72b7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -124,6 +124,7 @@
private final StatusBarIconController mStatusBarIconController;
private final CarrierConfigTracker mCarrierConfigTracker;
private final StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
+ private final StatusBarIconController.DarkIconManager.Factory mDarkIconManagerFactory;
private final SecureSettings mSecureSettings;
private final Executor mMainExecutor;
private final DumpManager mDumpManager;
@@ -172,6 +173,7 @@
PanelExpansionStateManager panelExpansionStateManager,
FeatureFlags featureFlags,
StatusBarIconController statusBarIconController,
+ StatusBarIconController.DarkIconManager.Factory darkIconManagerFactory,
StatusBarHideIconsForBouncerManager statusBarHideIconsForBouncerManager,
KeyguardStateController keyguardStateController,
NotificationPanelViewController notificationPanelViewController,
@@ -193,6 +195,7 @@
mFeatureFlags = featureFlags;
mStatusBarIconController = statusBarIconController;
mStatusBarHideIconsForBouncerManager = statusBarHideIconsForBouncerManager;
+ mDarkIconManagerFactory = darkIconManagerFactory;
mKeyguardStateController = keyguardStateController;
mNotificationPanelViewController = notificationPanelViewController;
mStatusBarStateController = statusBarStateController;
@@ -232,7 +235,7 @@
mStatusBar.restoreHierarchyState(
savedInstanceState.getSparseParcelableArray(EXTRA_PANEL_STATE));
}
- mDarkIconManager = new DarkIconManager(view.findViewById(R.id.statusIcons), mFeatureFlags);
+ mDarkIconManager = mDarkIconManagerFactory.create(view.findViewById(R.id.statusIcons));
mDarkIconManager.setShouldLog(true);
updateBlockedIcons();
mStatusBarIconController.addIconGroup(mDarkIconManager);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollector.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollector.kt
deleted file mode 100644
index 780a02d..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollector.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.pipeline
-
-import com.android.systemui.statusbar.pipeline.wifi.data.repository.NetworkCapabilityInfo
-import kotlinx.coroutines.flow.StateFlow
-
-/**
- * Interface exposing a flow for raw connectivity information. Clients should collect on
- * [rawConnectivityInfoFlow] to get updates on connectivity information.
- *
- * Note: [rawConnectivityInfoFlow] should be a *hot* flow, so that we only create one instance of it
- * and all clients get references to the same flow.
- *
- * This will be used for the new status bar pipeline to compile information we need to display some
- * of the icons in the RHS of the status bar.
- */
-interface ConnectivityInfoCollector {
- val rawConnectivityInfoFlow: StateFlow<RawConnectivityInfo>
-}
-
-/**
- * An object containing all of the raw connectivity information.
- *
- * Importantly, all the information in this object should not be processed at all (i.e., the data
- * that we receive from callbacks should be piped straight into this object and not be filtered,
- * manipulated, or processed in any way). Instead, any listeners on
- * [ConnectivityInfoCollector.rawConnectivityInfoFlow] can do the processing.
- *
- * This allows us to keep all the processing in one place which is beneficial for logging and
- * debugging purposes.
- */
-data class RawConnectivityInfo(
- val networkCapabilityInfo: Map<Int, NetworkCapabilityInfo> = emptyMap(),
-)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollectorImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollectorImpl.kt
deleted file mode 100644
index 99798f9..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollectorImpl.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.pipeline
-
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.statusbar.pipeline.wifi.data.repository.NetworkCapabilitiesRepo
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.SharingStarted.Companion.Lazily
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-
-/**
- * The real implementation of [ConnectivityInfoCollector] that will collect information from all the
- * relevant connectivity callbacks and compile it into [rawConnectivityInfoFlow].
- */
-@SysUISingleton
-class ConnectivityInfoCollectorImpl @Inject constructor(
- networkCapabilitiesRepo: NetworkCapabilitiesRepo,
- @Application scope: CoroutineScope,
-) : ConnectivityInfoCollector {
- override val rawConnectivityInfoFlow: StateFlow<RawConnectivityInfo> =
- // TODO(b/238425913): Collect all the separate flows for individual raw information into
- // this final flow.
- networkCapabilitiesRepo.dataStream
- .map {
- RawConnectivityInfo(networkCapabilityInfo = it)
- }
- .stateIn(scope, started = Lazily, initialValue = RawConnectivityInfo())
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt
index 64c47f6..fe84674 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt
@@ -20,31 +20,21 @@
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.statusbar.pipeline.wifi.data.repository.NetworkCapabilityInfo
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted.Companion.Lazily
import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
/**
- * A processor that transforms raw connectivity information that we get from callbacks and turns it
- * into a list of displayable connectivity information.
+ * A temporary object that collects on [WifiViewModel] flows for debugging purposes.
*
- * This will be used for the new status bar pipeline to calculate the list of icons that should be
- * displayed in the RHS of the status bar.
- *
- * Anyone can listen to [processedInfoFlow] to get updates to the processed data.
+ * This will eventually get migrated to a view binder that will use the flow outputs to set state on
+ * views. For now, this just collects on flows so that the information gets logged.
*/
@SysUISingleton
class ConnectivityInfoProcessor @Inject constructor(
- connectivityInfoCollector: ConnectivityInfoCollector,
context: Context,
// TODO(b/238425913): Don't use the application scope; instead, use the status bar view's
// scope so we only do work when there's UI that cares about it.
@@ -52,23 +42,8 @@
private val statusBarPipelineFlags: StatusBarPipelineFlags,
private val wifiViewModelProvider: Provider<WifiViewModel>,
) : CoreStartable(context) {
- // Note: This flow will not start running until a client calls `collect` on it, which means that
- // [connectivityInfoCollector]'s flow will also not start anything until that `collect` call
- // happens.
- // TODO(b/238425913): Delete this.
- val processedInfoFlow: Flow<ProcessedConnectivityInfo> =
- if (!statusBarPipelineFlags.isNewPipelineEnabled())
- emptyFlow()
- else connectivityInfoCollector.rawConnectivityInfoFlow
- .map { it.process() }
- .stateIn(
- scope,
- started = Lazily,
- initialValue = ProcessedConnectivityInfo()
- )
-
override fun start() {
- if (!statusBarPipelineFlags.isNewPipelineEnabled()) {
+ if (!statusBarPipelineFlags.isNewPipelineBackendEnabled()) {
return
}
// TODO(b/238425913): The view binder should do this instead. For now, do it here so we can
@@ -77,17 +52,4 @@
wifiViewModelProvider.get().isActivityInVisible.collect { }
}
}
-
- private fun RawConnectivityInfo.process(): ProcessedConnectivityInfo {
- // TODO(b/238425913): Actually process the raw info into meaningful data.
- return ProcessedConnectivityInfo(this.networkCapabilityInfo)
- }
}
-
-/**
- * An object containing connectivity info that has been processed into data that can be directly
- * used by the status bar (and potentially other SysUI areas) to display icons.
- */
-data class ProcessedConnectivityInfo(
- val networkCapabilityInfo: Map<Int, NetworkCapabilityInfo> = emptyMap(),
-)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
index 589cdb8..9b8b643 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
@@ -25,13 +25,28 @@
@SysUISingleton
class StatusBarPipelineFlags @Inject constructor(private val featureFlags: FeatureFlags) {
/**
- * Returns true if we should run the new pipeline.
+ * Returns true if we should run the new pipeline backend.
*
- * TODO(b/238425913): We may want to split this out into:
- * (1) isNewPipelineLoggingEnabled(), where the new pipeline runs and logs its decisions but
- * doesn't change the UI at all.
- * (2) isNewPipelineEnabled(), where the new pipeline runs and does change the UI (and the old
- * pipeline doesn't change the UI).
+ * The new pipeline backend hooks up to all our external callbacks, logs those callback inputs,
+ * and logs the output state.
*/
- fun isNewPipelineEnabled(): Boolean = featureFlags.isEnabled(Flags.NEW_STATUS_BAR_PIPELINE)
+ fun isNewPipelineBackendEnabled(): Boolean =
+ featureFlags.isEnabled(Flags.NEW_STATUS_BAR_PIPELINE_BACKEND)
+
+ /**
+ * Returns true if we should run the new pipeline frontend *and* backend.
+ *
+ * The new pipeline frontend will use the outputted state from the new backend and will make the
+ * correct changes to the UI.
+ */
+ fun isNewPipelineFrontendEnabled(): Boolean =
+ isNewPipelineBackendEnabled() &&
+ featureFlags.isEnabled(Flags.NEW_STATUS_BAR_PIPELINE_FRONTEND)
+
+ /**
+ * Returns true if we should apply some coloring to icons that were rendered with the new
+ * pipeline to help with debugging.
+ */
+ // For now, just always apply the debug coloring if we've enabled frontend rendering.
+ fun useNewPipelineDebugColoring(): Boolean = isNewPipelineFrontendEnabled()
}
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 7abe19e7b..88eccb5 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
@@ -17,8 +17,6 @@
package com.android.systemui.statusbar.pipeline.dagger
import com.android.systemui.CoreStartable
-import com.android.systemui.statusbar.pipeline.ConnectivityInfoCollector
-import com.android.systemui.statusbar.pipeline.ConnectivityInfoCollectorImpl
import com.android.systemui.statusbar.pipeline.ConnectivityInfoProcessor
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl
@@ -36,10 +34,5 @@
abstract fun bindConnectivityInfoProcessor(cip: ConnectivityInfoProcessor): CoreStartable
@Binds
- abstract fun provideConnectivityInfoCollector(
- impl: ConnectivityInfoCollectorImpl
- ): ConnectivityInfoCollector
-
- @Binds
abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
index a5fff5e..2a89309 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
@@ -31,6 +31,9 @@
class ConnectivityPipelineLogger @Inject constructor(
@StatusBarConnectivityLog private val buffer: LogBuffer,
) {
+ /**
+ * Logs a change in one of the **raw inputs** to the connectivity pipeline.
+ */
fun logInputChange(callbackName: String, changeInfo: String) {
buffer.log(
SB_LOGGING_TAG,
@@ -45,6 +48,41 @@
)
}
+ /**
+ * Logs a **data transformation** that we performed within the connectivity pipeline.
+ */
+ fun logTransformation(transformationName: String, oldValue: Any?, newValue: Any?) {
+ if (oldValue == newValue) {
+ buffer.log(
+ SB_LOGGING_TAG,
+ LogLevel.INFO,
+ {
+ str1 = transformationName
+ str2 = oldValue.toString()
+ },
+ {
+ "Transform: $str1: $str2 (transformation didn't change it)"
+ }
+ )
+ } else {
+ buffer.log(
+ SB_LOGGING_TAG,
+ LogLevel.INFO,
+ {
+ str1 = transformationName
+ str2 = oldValue.toString()
+ str3 = newValue.toString()
+ },
+ {
+ "Transform: $str1: $str2 -> $str3"
+ }
+ )
+ }
+ }
+
+ /**
+ * Logs a change in one of the **outputs** to the connectivity pipeline.
+ */
fun logOutputChange(outputParamName: String, changeInfo: String) {
buffer.log(
SB_LOGGING_TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiModel.kt
deleted file mode 100644
index 1b73322..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiModel.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.pipeline.wifi.data.model
-
-/** Provides information about the current wifi state. */
-data class WifiModel(
- /** See [android.net.wifi.WifiInfo.ssid]. */
- val ssid: String? = null,
- /** See [android.net.wifi.WifiInfo.isPasspointAp]. */
- val isPasspointAccessPoint: Boolean = false,
- /** See [android.net.wifi.WifiInfo.isOsuAp]. */
- val isOnlineSignUpForPasspointAccessPoint: Boolean = false,
- /** See [android.net.wifi.WifiInfo.passpointProviderFriendlyName]. */
- val passpointProviderFriendlyName: String? = null,
-)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
new file mode 100644
index 0000000..5566fa6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.wifi.data.model
+
+/** Provides information about the current wifi network. */
+sealed class WifiNetworkModel {
+ /** A model representing that we have no active wifi network. */
+ object Inactive : WifiNetworkModel()
+
+ /** Provides information about an active wifi network. */
+ class Active(
+ /**
+ * The [android.net.Network.netId] we received from
+ * [android.net.ConnectivityManager.NetworkCallback] in association with this wifi network.
+ *
+ * Importantly, **not** [android.net.wifi.WifiInfo.getNetworkId].
+ */
+ val networkId: Int,
+
+ /** See [android.net.wifi.WifiInfo.ssid]. */
+ val ssid: String? = null,
+
+ /** See [android.net.wifi.WifiInfo.isPasspointAp]. */
+ val isPasspointAccessPoint: Boolean = false,
+
+ /** See [android.net.wifi.WifiInfo.isOsuAp]. */
+ val isOnlineSignUpForPasspointAccessPoint: Boolean = false,
+
+ /** See [android.net.wifi.WifiInfo.passpointProviderFriendlyName]. */
+ val passpointProviderFriendlyName: String? = null,
+ ) : WifiNetworkModel()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepo.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepo.kt
deleted file mode 100644
index 6c0a445..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepo.kt
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:OptIn(ExperimentalCoroutinesApi::class)
-
-package com.android.systemui.statusbar.pipeline.wifi.data.repository
-
-import android.annotation.SuppressLint
-import android.net.ConnectivityManager
-import android.net.Network
-import android.net.NetworkCapabilities
-import android.net.NetworkRequest
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.SharingStarted.Companion.Lazily
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.flow.stateIn
-
-/**
- * Repository that contains all relevant [NetworkCapabilities] for the current networks.
- *
- * TODO(b/238425913): Figure out how to merge this with [WifiRepository].
- */
-@SysUISingleton
-class NetworkCapabilitiesRepo @Inject constructor(
- connectivityManager: ConnectivityManager,
- @Application scope: CoroutineScope,
- logger: ConnectivityPipelineLogger,
-) {
- @SuppressLint("MissingPermission")
- val dataStream: StateFlow<Map<Int, NetworkCapabilityInfo>> = run {
- var state = emptyMap<Int, NetworkCapabilityInfo>()
- callbackFlow {
- val networkRequest: NetworkRequest =
- NetworkRequest.Builder()
- .clearCapabilities()
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
- .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
- .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
- .build()
- val callback =
- // TODO (b/240569788): log these using [LogBuffer]
- object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
- override fun onCapabilitiesChanged(
- network: Network,
- networkCapabilities: NetworkCapabilities
- ) {
- logger.logOnCapabilitiesChanged(network, networkCapabilities)
- state =
- state.toMutableMap().also {
- it[network.getNetId()] =
- NetworkCapabilityInfo(network, networkCapabilities)
- }
- trySend(state)
- }
-
- override fun onLost(network: Network) {
- logger.logOnLost(network)
- state = state.toMutableMap().also { it.remove(network.getNetId()) }
- trySend(state)
- }
- }
- connectivityManager.registerNetworkCallback(networkRequest, callback)
-
- awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
- }
- .stateIn(scope, started = Lazily, initialValue = state)
- }
-}
-
-/** contains info about network capabilities. */
-data class NetworkCapabilityInfo(
- val network: Network,
- val capabilities: NetworkCapabilities,
-)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
index 012dde5..43fbabc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
@@ -16,16 +16,26 @@
package com.android.systemui.statusbar.pipeline.wifi.data.repository
+import android.annotation.SuppressLint
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
import android.net.wifi.WifiManager.TrafficStateCallback
import android.util.Log
+import com.android.settingslib.Utils
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel
-import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import java.util.concurrent.Executor
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -38,9 +48,9 @@
*/
interface WifiRepository {
/**
- * Observable for the current state of wifi; `null` when there is no active wifi.
+ * Observable for the current wifi network.
*/
- val wifiModel: Flow<WifiModel?>
+ val wifiNetwork: Flow<WifiNetworkModel>
/**
* Observable for the current wifi network activity.
@@ -51,14 +61,57 @@
/** Real implementation of [WifiRepository]. */
@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
+@SuppressLint("MissingPermission")
class WifiRepositoryImpl @Inject constructor(
+ connectivityManager: ConnectivityManager,
wifiManager: WifiManager?,
@Main mainExecutor: Executor,
logger: ConnectivityPipelineLogger,
) : WifiRepository {
+ override val wifiNetwork: Flow<WifiNetworkModel> = conflatedCallbackFlow {
+ var currentWifi: WifiNetworkModel = WIFI_NETWORK_DEFAULT
- // TODO(b/238425913): Actually implement the wifiModel flow.
- override val wifiModel: Flow<WifiModel?> = flowOf(WifiModel(ssid = "AB"))
+ val callback = object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
+ override fun onCapabilitiesChanged(
+ network: Network,
+ networkCapabilities: NetworkCapabilities
+ ) {
+ logger.logOnCapabilitiesChanged(network, networkCapabilities)
+
+ val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities)
+ if (wifiInfo?.isPrimary == true) {
+ val wifiNetworkModel = wifiInfoToModel(wifiInfo, network.getNetId())
+ logger.logTransformation(
+ WIFI_NETWORK_CALLBACK_NAME,
+ oldValue = currentWifi,
+ newValue = wifiNetworkModel
+ )
+ currentWifi = wifiNetworkModel
+ trySend(wifiNetworkModel)
+ }
+ }
+
+ override fun onLost(network: Network) {
+ logger.logOnLost(network)
+ val wifi = currentWifi
+ if (wifi is WifiNetworkModel.Active && wifi.networkId == network.getNetId()) {
+ val newNetworkModel = WifiNetworkModel.Inactive
+ logger.logTransformation(
+ WIFI_NETWORK_CALLBACK_NAME,
+ oldValue = wifi,
+ newValue = newNetworkModel
+ )
+ currentWifi = newNetworkModel
+ trySend(newNetworkModel)
+ }
+ }
+ }
+
+ trySend(WIFI_NETWORK_DEFAULT)
+ connectivityManager.registerNetworkCallback(WIFI_NETWORK_CALLBACK_REQUEST, callback)
+
+ awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
+ }
override val wifiActivity: Flow<WifiActivityModel> =
if (wifiManager == null) {
@@ -71,8 +124,8 @@
trySend(trafficStateToWifiActivityModel(state))
}
- wifiManager.registerTrafficStateCallback(mainExecutor, callback)
trySend(ACTIVITY_DEFAULT)
+ wifiManager.registerTrafficStateCallback(mainExecutor, callback)
awaitClose { wifiManager.unregisterTrafficStateCallback(callback) }
}
@@ -80,6 +133,13 @@
companion object {
val ACTIVITY_DEFAULT = WifiActivityModel(hasActivityIn = false, hasActivityOut = false)
+ // Start out with no known wifi network.
+ // Note: [WifiStatusTracker] (the old implementation of connectivity logic) does do an
+ // initial fetch to get a starting wifi network. But, it uses a deprecated API
+ // [WifiManager.getConnectionInfo()], and the deprecation doc indicates to just use
+ // [ConnectivityManager.NetworkCallback] results instead. So, for now we'll just rely on the
+ // NetworkCallback inside [wifiNetwork] for our wifi network information.
+ val WIFI_NETWORK_DEFAULT = WifiNetworkModel.Inactive
private fun trafficStateToWifiActivityModel(state: Int): WifiActivityModel {
return WifiActivityModel(
@@ -90,6 +150,30 @@
)
}
+ private fun networkCapabilitiesToWifiInfo(
+ networkCapabilities: NetworkCapabilities
+ ): WifiInfo? {
+ return when {
+ networkCapabilities.hasTransport(TRANSPORT_WIFI) ->
+ networkCapabilities.transportInfo as WifiInfo?
+ networkCapabilities.hasTransport(TRANSPORT_CELLULAR) ->
+ // Sometimes, cellular networks can act as wifi networks (known as VCN --
+ // virtual carrier network). So, see if this cellular network has wifi info.
+ Utils.tryGetWifiInfoForVcn(networkCapabilities)
+ else -> null
+ }
+ }
+
+ private fun wifiInfoToModel(wifiInfo: WifiInfo, networkId: Int): WifiNetworkModel {
+ return WifiNetworkModel.Active(
+ networkId,
+ wifiInfo.ssid,
+ wifiInfo.isPasspointAp,
+ wifiInfo.isOsuAp,
+ wifiInfo.passpointProviderFriendlyName
+ )
+ }
+
private fun prettyPrintActivity(activity: Int): String {
return when (activity) {
TrafficStateCallback.DATA_ACTIVITY_NONE -> "NONE"
@@ -99,5 +183,15 @@
else -> "INVALID"
}
}
+
+ private val WIFI_NETWORK_CALLBACK_REQUEST: NetworkRequest =
+ NetworkRequest.Builder()
+ .clearCapabilities()
+ .addCapability(NET_CAPABILITY_NOT_VPN)
+ .addTransportType(TRANSPORT_WIFI)
+ .addTransportType(TRANSPORT_CELLULAR)
+ .build()
+
+ private const val WIFI_NETWORK_CALLBACK_NAME = "wifiNetworkModel"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
index f705399..a9da63b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
@@ -18,6 +18,7 @@
import android.net.wifi.WifiManager
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
@@ -34,13 +35,15 @@
class WifiInteractor @Inject constructor(
repository: WifiRepository,
) {
- private val ssid: Flow<String?> = repository.wifiModel.map { info ->
- when {
- info == null -> null
- info.isPasspointAccessPoint || info.isOnlineSignUpForPasspointAccessPoint ->
- info.passpointProviderFriendlyName
- info.ssid != WifiManager.UNKNOWN_SSID -> info.ssid
- else -> null
+ private val ssid: Flow<String?> = repository.wifiNetwork.map { info ->
+ when (info) {
+ is WifiNetworkModel.Inactive -> null
+ is WifiNetworkModel.Active -> when {
+ info.isPasspointAccessPoint || info.isOnlineSignUpForPasspointAccessPoint ->
+ info.passpointProviderFriendlyName
+ info.ssid != WifiManager.UNKNOWN_SSID -> info.ssid
+ else -> null
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
new file mode 100644
index 0000000..b06aaf4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.wifi.ui.binder
+
+import android.content.res.ColorStateList
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.R
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS
+import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
+import kotlinx.coroutines.InternalCoroutinesApi
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+/**
+ * Binds a wifi icon in the status bar to its view-model.
+ *
+ * To use this properly, users should maintain a one-to-one relationship between the [View] and the
+ * view-binding, binding each view only once. It is okay and expected for the same instance of the
+ * view-model to be reused for multiple view/view-binder bindings.
+ */
+@OptIn(InternalCoroutinesApi::class)
+object WifiViewBinder {
+ /** Binds the view to the view-model, continuing to update the former based on the latter. */
+ @JvmStatic
+ fun bind(
+ view: ViewGroup,
+ viewModel: WifiViewModel,
+ ) {
+ val iconView = view.requireViewById<ImageView>(R.id.wifi_signal)
+
+ view.isVisible = true
+ iconView.isVisible = true
+ iconView.setImageDrawable(view.context.getDrawable(WIFI_FULL_ICONS[2]))
+
+ view.repeatWhenAttached {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch {
+ viewModel.tint.collect { tint ->
+ iconView.imageTintList = ColorStateList.valueOf(tint)
+ }
+ }
+ }
+ }
+
+ // TODO(b/238425913): Hook up to [viewModel] to render actual changes to the wifi icon.
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
new file mode 100644
index 0000000..c14a897
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.wifi.ui.view
+
+import android.content.Context
+import android.graphics.Rect
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import com.android.systemui.R
+import com.android.systemui.statusbar.BaseStatusBarWifiView
+import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
+import com.android.systemui.statusbar.pipeline.wifi.ui.binder.WifiViewBinder
+import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
+
+/**
+ * A new and more modern implementation of [com.android.systemui.statusbar.StatusBarWifiView] that
+ * is updated by [WifiViewBinder].
+ */
+class ModernStatusBarWifiView(
+ context: Context,
+ attrs: AttributeSet?
+) : BaseStatusBarWifiView(context, attrs) {
+
+ private lateinit var slot: String
+
+ override fun onDarkChanged(areas: ArrayList<Rect>?, darkIntensity: Float, tint: Int) {
+ // TODO(b/238425913)
+ }
+
+ override fun getSlot() = slot
+
+ override fun setStaticDrawableColor(color: Int) {
+ // TODO(b/238425913)
+ }
+
+ override fun setDecorColor(color: Int) {
+ // TODO(b/238425913)
+ }
+
+ override fun setVisibleState(state: Int, animate: Boolean) {
+ // TODO(b/238425913)
+ }
+
+ override fun getVisibleState(): Int {
+ // TODO(b/238425913)
+ return STATE_ICON
+ }
+
+ override fun isIconVisible(): Boolean {
+ // TODO(b/238425913)
+ return true
+ }
+
+ /** Set the slot name for this view. */
+ private fun setSlot(slotName: String) {
+ this.slot = slotName
+ }
+
+ companion object {
+ /**
+ * Inflates a new instance of [ModernStatusBarWifiView], binds it to [viewModel], and
+ * returns it.
+ */
+ @JvmStatic
+ fun constructAndBind(
+ context: Context,
+ slot: String,
+ viewModel: WifiViewModel,
+ ): ModernStatusBarWifiView {
+ return (
+ LayoutInflater.from(context).inflate(R.layout.new_status_bar_wifi_group, null)
+ as ModernStatusBarWifiView
+ ).also {
+ it.setSlot(slot)
+ WifiViewBinder.bind(it, viewModel)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
index b990eb7..7a26260 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
@@ -16,12 +16,15 @@
package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel
+import android.graphics.Color
+import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
/**
@@ -30,9 +33,10 @@
* TODO(b/238425913): Hook this up to the real status bar wifi view using a view binder.
*/
class WifiViewModel @Inject constructor(
- private val constants: WifiConstants,
- private val logger: ConnectivityPipelineLogger,
- private val interactor: WifiInteractor,
+ statusBarPipelineFlags: StatusBarPipelineFlags,
+ private val constants: WifiConstants,
+ private val logger: ConnectivityPipelineLogger,
+ private val interactor: WifiInteractor,
) {
val isActivityInVisible: Flow<Boolean>
get() =
@@ -42,4 +46,11 @@
interactor.hasActivityIn
}
.logOutputChange(logger, "activityInVisible")
+
+ /** The tint that should be applied to the icon. */
+ val tint: Flow<Int> = if (!statusBarPipelineFlags.useNewPipelineDebugColoring()) {
+ emptyFlow()
+ } else {
+ flowOf(Color.CYAN)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
index dfcdaef..836d571 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
@@ -45,7 +45,6 @@
import android.provider.Settings;
import android.telephony.TelephonyCallback;
import android.text.TextUtils;
-import android.util.FeatureFlagUtils;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
@@ -287,10 +286,6 @@
refreshUsers(UserHandle.USER_NULL);
}
- private static boolean isEnableGuestModeUxChanges(Context context) {
- return FeatureFlagUtils.isEnabled(context, FeatureFlagUtils.SETTINGS_GUEST_MODE_UX_CHANGES);
- }
-
/**
* Refreshes users from UserManager.
*
@@ -549,17 +544,9 @@
}
if (currUserInfo != null && currUserInfo.isGuest()) {
- if (isEnableGuestModeUxChanges(mContext)) {
- showExitGuestDialog(currUserId, currUserInfo.isEphemeral(),
- record.resolveId(), dialogShower);
- return;
- } else {
- if (currUserInfo.isEphemeral()) {
- showExitGuestDialog(currUserId, currUserInfo.isEphemeral(),
- record.resolveId(), dialogShower);
- return;
- }
- }
+ showExitGuestDialog(currUserId, currUserInfo.isEphemeral(),
+ record.resolveId(), dialogShower);
+ return;
}
if (dialogShower != null) {
@@ -1056,14 +1043,8 @@
public String getName(Context context, UserRecord item) {
if (item.isGuest) {
if (item.isCurrent) {
- if (isEnableGuestModeUxChanges(context)) {
- return context.getString(
- com.android.settingslib.R.string.guest_exit_quick_settings_button);
- } else {
- return context.getString(mController.mGuestUserAutoCreated
- ? com.android.settingslib.R.string.guest_reset_guest
- : com.android.settingslib.R.string.guest_exit_guest);
- }
+ return context.getString(
+ com.android.settingslib.R.string.guest_exit_quick_settings_button);
} else {
if (item.info != null) {
return context.getString(com.android.internal.R.string.guest_name);
@@ -1080,13 +1061,8 @@
? com.android.settingslib.R.string.guest_resetting
: com.android.internal.R.string.guest_name);
} else {
- if (isEnableGuestModeUxChanges(context)) {
- // we always show "guest" as string, instead of "add guest"
- return context.getString(com.android.internal.R.string.guest_name);
- } else {
- return context.getString(
- com.android.settingslib.R.string.guest_new_guest);
- }
+ // we always show "guest" as string, instead of "add guest"
+ return context.getString(com.android.internal.R.string.guest_name);
}
}
}
@@ -1108,11 +1084,7 @@
protected static Drawable getIconDrawable(Context context, UserRecord item) {
int iconRes;
if (item.isAddUser) {
- if (isEnableGuestModeUxChanges(context)) {
- iconRes = R.drawable.ic_add;
- } else {
- iconRes = R.drawable.ic_account_circle_filled;
- }
+ iconRes = R.drawable.ic_add;
} else if (item.isGuest) {
iconRes = R.drawable.ic_account_circle;
} else if (item.isAddSupervisedUser) {
@@ -1289,46 +1261,32 @@
ExitGuestDialog(Context context, int guestId, boolean isGuestEphemeral,
int targetId) {
super(context);
- if (isEnableGuestModeUxChanges(context)) {
- if (isGuestEphemeral) {
- setTitle(context.getString(
- com.android.settingslib.R.string.guest_exit_dialog_title));
- setMessage(context.getString(
- com.android.settingslib.R.string.guest_exit_dialog_message));
- setButton(DialogInterface.BUTTON_NEUTRAL,
- context.getString(android.R.string.cancel), this);
- setButton(DialogInterface.BUTTON_POSITIVE,
- context.getString(
- com.android.settingslib.R.string.guest_exit_dialog_button), this);
- } else {
- setTitle(context.getString(
- com.android.settingslib
- .R.string.guest_exit_dialog_title_non_ephemeral));
- setMessage(context.getString(
- com.android.settingslib
- .R.string.guest_exit_dialog_message_non_ephemeral));
- setButton(DialogInterface.BUTTON_NEUTRAL,
- context.getString(android.R.string.cancel), this);
- setButton(DialogInterface.BUTTON_NEGATIVE,
- context.getString(
- com.android.settingslib.R.string.guest_exit_clear_data_button),
- this);
- setButton(DialogInterface.BUTTON_POSITIVE,
- context.getString(
- com.android.settingslib.R.string.guest_exit_save_data_button),
- this);
- }
- } else {
- setTitle(mGuestUserAutoCreated
- ? com.android.settingslib.R.string.guest_reset_guest_dialog_title
- : com.android.settingslib.R.string.guest_remove_guest_dialog_title);
- setMessage(context.getString(R.string.guest_exit_guest_dialog_message));
+ if (isGuestEphemeral) {
+ setTitle(context.getString(
+ com.android.settingslib.R.string.guest_exit_dialog_title));
+ setMessage(context.getString(
+ com.android.settingslib.R.string.guest_exit_dialog_message));
setButton(DialogInterface.BUTTON_NEUTRAL,
context.getString(android.R.string.cancel), this);
setButton(DialogInterface.BUTTON_POSITIVE,
- context.getString(mGuestUserAutoCreated
- ? com.android.settingslib.R.string.guest_reset_guest_confirm_button
- : com.android.settingslib.R.string.guest_remove_guest_confirm_button),
+ context.getString(
+ com.android.settingslib.R.string.guest_exit_dialog_button), this);
+ } else {
+ setTitle(context.getString(
+ com.android.settingslib
+ .R.string.guest_exit_dialog_title_non_ephemeral));
+ setMessage(context.getString(
+ com.android.settingslib
+ .R.string.guest_exit_dialog_message_non_ephemeral));
+ setButton(DialogInterface.BUTTON_NEUTRAL,
+ context.getString(android.R.string.cancel), this);
+ setButton(DialogInterface.BUTTON_NEGATIVE,
+ context.getString(
+ com.android.settingslib.R.string.guest_exit_clear_data_button),
+ this);
+ setButton(DialogInterface.BUTTON_POSITIVE,
+ context.getString(
+ com.android.settingslib.R.string.guest_exit_save_data_button),
this);
}
SystemUIDialog.setWindowOnTop(this, mKeyguardStateController.isShowing());
@@ -1345,39 +1303,29 @@
if (mFalsingManager.isFalseTap(penalty)) {
return;
}
- if (isEnableGuestModeUxChanges(getContext())) {
- if (mIsGuestEphemeral) {
- if (which == DialogInterface.BUTTON_POSITIVE) {
- mDialogLaunchAnimator.dismissStack(this);
- // Ephemeral guest: exit guest, guest is removed by the system
- // on exit, since its marked ephemeral
- exitGuestUser(mGuestId, mTargetId, false);
- } else if (which == DialogInterface.BUTTON_NEGATIVE) {
- // Cancel clicked, do nothing
- cancel();
- }
- } else {
- if (which == DialogInterface.BUTTON_POSITIVE) {
- mDialogLaunchAnimator.dismissStack(this);
- // Non-ephemeral guest: exit guest, guest is not removed by the system
- // on exit, since its marked non-ephemeral
- exitGuestUser(mGuestId, mTargetId, false);
- } else if (which == DialogInterface.BUTTON_NEGATIVE) {
- mDialogLaunchAnimator.dismissStack(this);
- // Non-ephemeral guest: remove guest and then exit
- exitGuestUser(mGuestId, mTargetId, true);
- } else if (which == DialogInterface.BUTTON_NEUTRAL) {
- // Cancel clicked, do nothing
- cancel();
- }
+ if (mIsGuestEphemeral) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ mDialogLaunchAnimator.dismissStack(this);
+ // Ephemeral guest: exit guest, guest is removed by the system
+ // on exit, since its marked ephemeral
+ exitGuestUser(mGuestId, mTargetId, false);
+ } else if (which == DialogInterface.BUTTON_NEGATIVE) {
+ // Cancel clicked, do nothing
+ cancel();
}
} else {
- if (which == BUTTON_NEUTRAL) {
- cancel();
- } else {
- mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE);
+ if (which == DialogInterface.BUTTON_POSITIVE) {
mDialogLaunchAnimator.dismissStack(this);
- removeGuestUser(mGuestId, mTargetId);
+ // Non-ephemeral guest: exit guest, guest is not removed by the system
+ // on exit, since its marked non-ephemeral
+ exitGuestUser(mGuestId, mTargetId, false);
+ } else if (which == DialogInterface.BUTTON_NEGATIVE) {
+ mDialogLaunchAnimator.dismissStack(this);
+ // Non-ephemeral guest: remove guest and then exit
+ exitGuestUser(mGuestId, mTargetId, true);
+ } else if (which == DialogInterface.BUTTON_NEUTRAL) {
+ // Cancel clicked, do nothing
+ cancel();
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/IpcSerializer.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/IpcSerializer.kt
new file mode 100644
index 0000000..c0331e6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/IpcSerializer.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.kotlin
+
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.runBlocking
+
+/**
+ * A utility for handling incoming IPCs from a Binder interface in the order that they are received.
+ *
+ * This class serves as a replacement for the common [android.os.Handler] message-queue pattern,
+ * where IPCs can arrive on arbitrary threads and are all enqueued onto a queue and processed by the
+ * Handler in-order.
+ *
+ * class MyService : Service() {
+ *
+ * private val serializer = IpcSerializer()
+ *
+ * // Need to invoke process() in order to actually process IPCs sent over the serializer.
+ * override fun onStart(...) = lifecycleScope.launch {
+ * serializer.process()
+ * }
+ *
+ * // In your binder implementation, use runSerializedBlocking to enqueue a function onto
+ * // the serializer.
+ * override fun onBind(intent: Intent?) = object : IAidlService.Stub() {
+ * override fun ipcMethodFoo() = serializer.runSerializedBlocking {
+ * ...
+ * }
+ *
+ * override fun ipcMethodBar() = serializer.runSerializedBlocking {
+ * ...
+ * }
+ * }
+ * }
+ */
+class IpcSerializer {
+
+ private val channel = Channel<Pair<CompletableDeferred<Unit>, Job>>()
+
+ /**
+ * Runs functions enqueued via usage of [runSerialized] and [runSerializedBlocking] serially.
+ * This method will never complete normally, so it must be launched in its own coroutine; if
+ * this is not actively running, no enqueued functions will be evaluated.
+ */
+ suspend fun process(): Nothing {
+ for ((start, finish) in channel) {
+ // Signal to the sender that serializer has reached this message
+ start.complete(Unit)
+ // Wait to hear from the sender that it has finished running it's work, before handling
+ // the next message
+ finish.join()
+ }
+ error("Unexpected end of serialization channel")
+ }
+
+ /**
+ * Enqueues [block] for evaluation by the serializer, suspending the caller until it has
+ * completed. It is up to the caller to define what thread this is evaluated in, determined
+ * by the [kotlin.coroutines.CoroutineContext] used.
+ */
+ suspend fun <R> runSerialized(block: suspend () -> R): R {
+ val start = CompletableDeferred(Unit)
+ val finish = CompletableDeferred(Unit)
+ // Enqueue our message on the channel.
+ channel.send(start to finish)
+ // Wait for the serializer to reach our message
+ start.await()
+ // Now evaluate the block
+ val result = block()
+ // Notify the serializer that we've completed evaluation
+ finish.complete(Unit)
+ return result
+ }
+
+ /**
+ * Enqueues [block] for evaluation by the serializer, blocking the binder thread until it has
+ * completed. Evaluation occurs on the binder thread, so methods like
+ * [android.os.Binder.getCallingUid] that depend on the current thread will work as expected.
+ */
+ fun <R> runSerializedBlocking(block: suspend () -> R): R = runBlocking { runSerialized(block) }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
index aeab2df..199048e 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
@@ -50,9 +50,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.IStatusBarService;
-import com.android.systemui.Dumpable;
import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dump.DumpManager;
import com.android.systemui.model.SysUiState;
import com.android.systemui.shade.ShadeController;
import com.android.systemui.shared.system.QuickStepContract;
@@ -77,7 +75,6 @@
import com.android.wm.shell.bubbles.BubbleEntry;
import com.android.wm.shell.bubbles.Bubbles;
-import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -92,7 +89,7 @@
* The SysUi side bubbles manager which communicate with other SysUi components.
*/
@SysUISingleton
-public class BubblesManager implements Dumpable {
+public class BubblesManager {
private static final String TAG = TAG_WITH_CLASS_NAME ? "BubblesManager" : TAG_BUBBLES;
@@ -137,7 +134,6 @@
CommonNotifCollection notifCollection,
NotifPipeline notifPipeline,
SysUiState sysUiState,
- DumpManager dumpManager,
Executor sysuiMainExecutor) {
if (bubblesOptional.isPresent()) {
return new BubblesManager(context,
@@ -156,7 +152,6 @@
notifCollection,
notifPipeline,
sysUiState,
- dumpManager,
sysuiMainExecutor);
} else {
return null;
@@ -180,7 +175,6 @@
CommonNotifCollection notifCollection,
NotifPipeline notifPipeline,
SysUiState sysUiState,
- DumpManager dumpManager,
Executor sysuiMainExecutor) {
mContext = context;
mBubbles = bubbles;
@@ -203,8 +197,6 @@
setupNotifPipeline();
- dumpManager.registerDumpable(TAG, this);
-
keyguardStateController.addCallback(new KeyguardStateController.Callback() {
@Override
public void onKeyguardShowingChanged() {
@@ -648,11 +640,6 @@
}
}
- @Override
- public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
- mBubbles.dump(pw, args);
- }
-
/** Checks whether bubbles are enabled for this user, handles negative userIds. */
public static boolean areBubblesEnabled(@NonNull Context context, @NonNull UserHandle user) {
if (user.getIdentifier() < 0) {
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index eba2795..a4a59fc 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -29,14 +29,16 @@
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED;
import android.content.Context;
+import android.content.pm.UserInfo;
import android.content.res.Configuration;
import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
import android.inputmethodservice.InputMethodService;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.view.KeyEvent;
+import androidx.annotation.NonNull;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.keyguard.KeyguardUpdateMonitorCallback;
@@ -47,11 +49,11 @@
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.model.SysUiState;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.shared.tracing.ProtoTraceable;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.statusbar.policy.UserInfoController;
import com.android.systemui.tracing.ProtoTracer;
import com.android.systemui.tracing.nano.SystemUiTraceProto;
import com.android.wm.shell.nano.WmShellTraceProto;
@@ -66,6 +68,7 @@
import java.io.PrintWriter;
import java.util.Arrays;
+import java.util.List;
import java.util.Optional;
import java.util.concurrent.Executor;
@@ -115,7 +118,7 @@
private final SysUiState mSysUiState;
private final WakefulnessLifecycle mWakefulnessLifecycle;
private final ProtoTracer mProtoTracer;
- private final UserInfoController mUserInfoController;
+ private final UserTracker mUserTracker;
private final Executor mSysUiMainExecutor;
// Listeners and callbacks. Note that we prefer member variable over anonymous class here to
@@ -144,9 +147,20 @@
mShell.onKeyguardDismissAnimationFinished();
}
};
+ private final UserTracker.Callback mUserChangedCallback =
+ new UserTracker.Callback() {
+ @Override
+ public void onUserChanged(int newUser, @NonNull Context userContext) {
+ mShell.onUserChanged(newUser, userContext);
+ }
+
+ @Override
+ public void onProfilesChanged(@NonNull List<UserInfo> profiles) {
+ mShell.onUserProfilesChanged(profiles);
+ }
+ };
private boolean mIsSysUiStateValid;
- private KeyguardUpdateMonitorCallback mOneHandedKeyguardCallback;
private WakefulnessLifecycle.Observer mWakefulnessObserver;
@Inject
@@ -163,7 +177,7 @@
SysUiState sysUiState,
ProtoTracer protoTracer,
WakefulnessLifecycle wakefulnessLifecycle,
- UserInfoController userInfoController,
+ UserTracker userTracker,
@Main Executor sysUiMainExecutor) {
super(context);
mShell = shell;
@@ -178,7 +192,7 @@
mOneHandedOptional = oneHandedOptional;
mWakefulnessLifecycle = wakefulnessLifecycle;
mProtoTracer = protoTracer;
- mUserInfoController = userInfoController;
+ mUserTracker = userTracker;
mSysUiMainExecutor = sysUiMainExecutor;
}
@@ -192,8 +206,9 @@
mKeyguardStateController.addCallback(mKeyguardStateCallback);
mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);
- // TODO: Consider piping config change and other common calls to a shell component to
- // delegate internally
+ // Subscribe to user changes
+ mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor());
+
mProtoTracer.add(this);
mCommandQueue.addCallback(this);
mPipOptional.ifPresent(this::initPip);
@@ -214,10 +229,6 @@
mIsSysUiStateValid = (sysUiStateFlag & INVALID_SYSUI_STATE_MASK) == 0;
pip.onSystemUiStateChanged(mIsSysUiStateValid, sysUiStateFlag);
});
-
- // The media session listener needs to be re-registered when switching users
- mUserInfoController.addCallback((String name, Drawable picture, String userAccount) ->
- pip.registerSessionListenerForCurrentUser());
}
@VisibleForTesting
@@ -267,15 +278,6 @@
}
});
- // TODO: Either move into ShellInterface or register a receiver on the Shell side directly
- mOneHandedKeyguardCallback = new KeyguardUpdateMonitorCallback() {
- @Override
- public void onUserSwitchComplete(int userId) {
- oneHanded.onUserSwitch(userId);
- }
- };
- mKeyguardUpdateMonitor.registerCallback(mOneHandedKeyguardCallback);
-
mWakefulnessObserver =
new WakefulnessLifecycle.Observer() {
@Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
index 01309f8..7f6b79b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
@@ -308,6 +308,12 @@
}
@Test
+ public void testOnViewDetachedRemovesViews() {
+ mController.onViewDetached();
+ verify(mView).removeAllStatusBarItemViews();
+ }
+
+ @Test
public void testWifiIconHiddenWhenWifiBecomesAvailable() {
// Make sure wifi starts out unavailable when onViewAttached is called, and then returns
// true on the second query.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
index 19491f4..14b85b8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
@@ -37,6 +37,8 @@
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
+import kotlin.math.max
+import kotlin.math.min
import kotlin.reflect.KClass
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -127,6 +129,7 @@
val testConfig =
TestConfig(
isVisible = true,
+ isClickable = true,
icon = mock(),
canShowWhileLocked = false,
intent = Intent("action"),
@@ -154,6 +157,7 @@
val config =
TestConfig(
isVisible = true,
+ isClickable = true,
icon = mock(),
canShowWhileLocked = false,
intent = null, // This will cause it to tell the system that the click was handled.
@@ -201,6 +205,7 @@
val testConfig =
TestConfig(
isVisible = true,
+ isClickable = true,
icon = mock(),
canShowWhileLocked = false,
intent = Intent("action"),
@@ -260,6 +265,7 @@
testConfig =
TestConfig(
isVisible = true,
+ isClickable = true,
icon = mock(),
canShowWhileLocked = true,
)
@@ -269,6 +275,7 @@
testConfig =
TestConfig(
isVisible = true,
+ isClickable = true,
icon = mock(),
canShowWhileLocked = false,
)
@@ -342,6 +349,129 @@
job.cancel()
}
+ @Test
+ fun `isClickable - true when alpha at threshold`() = runBlockingTest {
+ repository.setKeyguardShowing(true)
+ repository.setBottomAreaAlpha(
+ KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD
+ )
+
+ val testConfig =
+ TestConfig(
+ isVisible = true,
+ isClickable = true,
+ icon = mock(),
+ canShowWhileLocked = false,
+ intent = Intent("action"),
+ )
+ val configKey =
+ setUpQuickAffordanceModel(
+ position = KeyguardQuickAffordancePosition.BOTTOM_START,
+ testConfig = testConfig,
+ )
+
+ var latest: KeyguardQuickAffordanceViewModel? = null
+ val job = underTest.startButton.onEach { latest = it }.launchIn(this)
+
+ assertQuickAffordanceViewModel(
+ viewModel = latest,
+ testConfig = testConfig,
+ configKey = configKey,
+ )
+ job.cancel()
+ }
+
+ @Test
+ fun `isClickable - true when alpha above threshold`() = runBlockingTest {
+ repository.setKeyguardShowing(true)
+ var latest: KeyguardQuickAffordanceViewModel? = null
+ val job = underTest.startButton.onEach { latest = it }.launchIn(this)
+ repository.setBottomAreaAlpha(
+ min(1f, KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD + 0.1f),
+ )
+
+ val testConfig =
+ TestConfig(
+ isVisible = true,
+ isClickable = true,
+ icon = mock(),
+ canShowWhileLocked = false,
+ intent = Intent("action"),
+ )
+ val configKey =
+ setUpQuickAffordanceModel(
+ position = KeyguardQuickAffordancePosition.BOTTOM_START,
+ testConfig = testConfig,
+ )
+
+ assertQuickAffordanceViewModel(
+ viewModel = latest,
+ testConfig = testConfig,
+ configKey = configKey,
+ )
+ job.cancel()
+ }
+
+ @Test
+ fun `isClickable - false when alpha below threshold`() = runBlockingTest {
+ repository.setKeyguardShowing(true)
+ var latest: KeyguardQuickAffordanceViewModel? = null
+ val job = underTest.startButton.onEach { latest = it }.launchIn(this)
+ repository.setBottomAreaAlpha(
+ max(0f, KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD - 0.1f),
+ )
+
+ val testConfig =
+ TestConfig(
+ isVisible = true,
+ isClickable = false,
+ icon = mock(),
+ canShowWhileLocked = false,
+ intent = Intent("action"),
+ )
+ val configKey =
+ setUpQuickAffordanceModel(
+ position = KeyguardQuickAffordancePosition.BOTTOM_START,
+ testConfig = testConfig,
+ )
+
+ assertQuickAffordanceViewModel(
+ viewModel = latest,
+ testConfig = testConfig,
+ configKey = configKey,
+ )
+ job.cancel()
+ }
+
+ @Test
+ fun `isClickable - false when alpha at zero`() = runBlockingTest {
+ repository.setKeyguardShowing(true)
+ var latest: KeyguardQuickAffordanceViewModel? = null
+ val job = underTest.startButton.onEach { latest = it }.launchIn(this)
+ repository.setBottomAreaAlpha(0f)
+
+ val testConfig =
+ TestConfig(
+ isVisible = true,
+ isClickable = false,
+ icon = mock(),
+ canShowWhileLocked = false,
+ intent = Intent("action"),
+ )
+ val configKey =
+ setUpQuickAffordanceModel(
+ position = KeyguardQuickAffordancePosition.BOTTOM_START,
+ testConfig = testConfig,
+ )
+
+ assertQuickAffordanceViewModel(
+ viewModel = latest,
+ testConfig = testConfig,
+ configKey = configKey,
+ )
+ job.cancel()
+ }
+
private suspend fun setDozeAmountAndCalculateExpectedTranslationY(dozeAmount: Float): Float {
repository.setDozeAmount(dozeAmount)
return dozeAmount * (RETURNED_BURN_IN_OFFSET - DEFAULT_BURN_IN_OFFSET)
@@ -384,6 +514,7 @@
) {
checkNotNull(viewModel)
assertThat(viewModel.isVisible).isEqualTo(testConfig.isVisible)
+ assertThat(viewModel.isClickable).isEqualTo(testConfig.isClickable)
if (testConfig.isVisible) {
assertThat(viewModel.icon).isEqualTo(testConfig.icon)
viewModel.onClicked.invoke(
@@ -404,6 +535,7 @@
private data class TestConfig(
val isVisible: Boolean,
+ val isClickable: Boolean = false,
val icon: ContainedDrawable? = null,
val canShowWhileLocked: Boolean = false,
val intent: Intent? = null,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/InstantTaskExecutorRule.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/InstantTaskExecutorRule.kt
new file mode 100644
index 0000000..373af5c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/InstantTaskExecutorRule.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.lifecycle
+
+import androidx.arch.core.executor.ArchTaskExecutor
+import androidx.arch.core.executor.TaskExecutor
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+
+/**
+ * Test rule that makes ArchTaskExecutor main thread assertions pass. There is one such assert
+ * in LifecycleRegistry.
+ */
+class InstantTaskExecutorRule : TestWatcher() {
+ // TODO(b/240620122): This is a copy of
+ // androidx/arch/core/executor/testing/InstantTaskExecutorRule which should be replaced
+ // with a dependency on the real library once b/ is cleared.
+ override fun starting(description: Description) {
+ super.starting(description)
+ ArchTaskExecutor.getInstance()
+ .setDelegate(
+ object : TaskExecutor() {
+ override fun executeOnDiskIO(runnable: Runnable) {
+ runnable.run()
+ }
+
+ override fun postToMainThread(runnable: Runnable) {
+ runnable.run()
+ }
+
+ override fun isMainThread(): Boolean {
+ return true
+ }
+ }
+ )
+ }
+
+ override fun finished(description: Description) {
+ super.finished(description)
+ ArchTaskExecutor.getInstance().setDelegate(null)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt
index 80f3e46..91a6de6a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt
@@ -20,8 +20,6 @@
import android.testing.TestableLooper.RunWithLooper
import android.view.View
import android.view.ViewTreeObserver
-import androidx.arch.core.executor.ArchTaskExecutor
-import androidx.arch.core.executor.TaskExecutor
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.test.filters.SmallTest
@@ -35,8 +33,6 @@
import org.junit.Before
import org.junit.Rule
import org.junit.Test
-import org.junit.rules.TestWatcher
-import org.junit.runner.Description
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mock
@@ -282,38 +278,4 @@
_invocations.add(Invocation(lifecycleOwner))
}
}
-
- /**
- * Test rule that makes ArchTaskExecutor main thread assertions pass. There is one such assert
- * in LifecycleRegistry.
- */
- class InstantTaskExecutorRule : TestWatcher() {
- // TODO(b/240620122): This is a copy of
- // androidx/arch/core/executor/testing/InstantTaskExecutorRule which should be replaced
- // with a dependency on the real library once b/ is cleared.
- override fun starting(description: Description) {
- super.starting(description)
- ArchTaskExecutor.getInstance()
- .setDelegate(
- object : TaskExecutor() {
- override fun executeOnDiskIO(runnable: Runnable) {
- runnable.run()
- }
-
- override fun postToMainThread(runnable: Runnable) {
- runnable.run()
- }
-
- override fun isMainThread(): Boolean {
- return true
- }
- }
- )
- }
-
- override fun finished(description: Description) {
- super.finished(description)
- ArchTaskExecutor.getInstance().setDelegate(null)
- }
- }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
index 260bb87..22ecb4b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -78,7 +78,7 @@
when(mMediaOutputController.getMediaDevices()).thenReturn(mMediaDevices);
when(mMediaOutputController.hasAdjustVolumeUserRestriction()).thenReturn(false);
- when(mMediaOutputController.isTransferring()).thenReturn(false);
+ when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(false);
when(mMediaOutputController.getDeviceIconCompat(mMediaDevice1)).thenReturn(mIconCompat);
when(mMediaOutputController.getDeviceIconCompat(mMediaDevice2)).thenReturn(mIconCompat);
when(mMediaOutputController.getCurrentConnectedMediaDevice()).thenReturn(mMediaDevice1);
@@ -208,7 +208,7 @@
@Test
public void onBindViewHolder_inTransferring_bindTransferringDevice_verifyView() {
- when(mMediaOutputController.isTransferring()).thenReturn(true);
+ when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(true);
when(mMediaDevice1.getState()).thenReturn(
LocalMediaManager.MediaDeviceState.STATE_CONNECTING);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -224,7 +224,7 @@
@Test
public void onBindViewHolder_inTransferring_bindNonTransferringDevice_verifyView() {
- when(mMediaOutputController.isTransferring()).thenReturn(true);
+ when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(true);
when(mMediaDevice2.getState()).thenReturn(
LocalMediaManager.MediaDeviceState.STATE_CONNECTING);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
index be14cc5..cb4f08e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
@@ -93,6 +93,10 @@
private lateinit var featureFlags: FeatureFlags
@Mock
private lateinit var insetsProvider: StatusBarContentInsetsProvider
+ @Mock
+ private lateinit var iconManagerFactory: StatusBarIconController.TintedIconManager.Factory
+ @Mock
+ private lateinit var iconManager: StatusBarIconController.TintedIconManager
private val qsExpansionPathInterpolator = QSExpansionPathInterpolator()
@@ -106,6 +110,7 @@
`when`(qsCarrierGroupControllerBuilder.build()).thenReturn(qsCarrierGroupController)
`when`(variableDateViewControllerFactory.create(any()))
.thenReturn(variableDateViewController)
+ `when`(iconManagerFactory.create(any())).thenReturn(iconManager)
`when`(view.resources).thenReturn(mContext.resources)
`when`(view.isAttachedToWindow).thenReturn(true)
`when`(view.context).thenReturn(context)
@@ -122,7 +127,8 @@
featureFlags,
variableDateViewControllerFactory,
batteryMeterViewController,
- insetsProvider
+ insetsProvider,
+ iconManagerFactory,
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
index ed1a13b..20c6d9a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
@@ -91,6 +91,10 @@
@Mock
private lateinit var statusBarIconController: StatusBarIconController
@Mock
+ private lateinit var iconManagerFactory: StatusBarIconController.TintedIconManager.Factory
+ @Mock
+ private lateinit var iconManager: StatusBarIconController.TintedIconManager
+ @Mock
private lateinit var qsCarrierGroupController: QSCarrierGroupController
@Mock
private lateinit var qsCarrierGroupControllerBuilder: QSCarrierGroupController.Builder
@@ -169,6 +173,8 @@
}
whenever(view.visibility).thenAnswer { _ -> viewVisibility }
+ whenever(iconManagerFactory.create(any())).thenReturn(iconManager)
+
whenever(featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS)).thenReturn(true)
whenever(featureFlags.isEnabled(Flags.NEW_HEADER)).thenReturn(true)
@@ -178,6 +184,7 @@
controller = LargeScreenShadeHeaderController(
view,
statusBarIconController,
+ iconManagerFactory,
privacyIconsController,
insetsProvider,
configurationController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt
index 02b26db..eeb61bc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt
@@ -43,6 +43,8 @@
@Mock private lateinit var view: View
@Mock private lateinit var statusIcons: StatusIconContainer
@Mock private lateinit var statusBarIconController: StatusBarIconController
+ @Mock private lateinit var iconManagerFactory: StatusBarIconController.TintedIconManager.Factory
+ @Mock private lateinit var iconManager: StatusBarIconController.TintedIconManager
@Mock private lateinit var qsCarrierGroupController: QSCarrierGroupController
@Mock private lateinit var qsCarrierGroupControllerBuilder: QSCarrierGroupController.Builder
@Mock private lateinit var featureFlags: FeatureFlags
@@ -91,10 +93,12 @@
whenever(view.visibility).thenAnswer { _ -> viewVisibility }
whenever(variableDateViewControllerFactory.create(any()))
.thenReturn(variableDateViewController)
+ whenever(iconManagerFactory.create(any())).thenReturn(iconManager)
whenever(featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS)).thenReturn(false)
mLargeScreenShadeHeaderController = LargeScreenShadeHeaderController(
view,
statusBarIconController,
+ iconManagerFactory,
privacyIconsController,
insetsProvider,
configurationController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/rotation/RotationButtonControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/rotation/RotationButtonControllerTest.kt
new file mode 100644
index 0000000..9393a4f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/rotation/RotationButtonControllerTest.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.shared.rotation
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.view.Display
+import android.view.WindowInsetsController
+import android.view.WindowManagerPolicyConstants
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+@RunWithLooper
+class RotationButtonControllerTest : SysuiTestCase() {
+
+ private lateinit var mController: RotationButtonController
+
+ @Before
+ fun setUp() {
+ mController = RotationButtonController(
+ mContext,
+ /* lightIconColor = */ 0,
+ /* darkIconColor = */ 0,
+ /* iconCcwStart0ResId = */ 0,
+ /* iconCcwStart90ResId = */ 0,
+ /* iconCwStart0ResId = */ 0,
+ /* iconCwStart90ResId = */ 0
+ ) { 0 }
+ }
+
+ @Test
+ fun ifGestural_showRotationSuggestion() {
+ mController.onNavigationBarWindowVisibilityChange( /* showing = */ false)
+ mController.onBehaviorChanged(Display.DEFAULT_DISPLAY,
+ WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE)
+ mController.onNavigationModeChanged(WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON)
+ mController.onTaskbarStateChange( /* visible = */ false, /* stashed = */ false)
+ assertThat(mController.canShowRotationButton()).isFalse()
+
+ mController.onNavigationModeChanged(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL)
+
+ assertThat(mController.canShowRotationButton()).isTrue()
+ }
+
+ @Test
+ fun ifTaskbarVisible_showRotationSuggestion() {
+ mController.onNavigationBarWindowVisibilityChange( /* showing = */ false)
+ mController.onBehaviorChanged(Display.DEFAULT_DISPLAY,
+ WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE)
+ mController.onNavigationModeChanged(WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON)
+ mController.onTaskbarStateChange( /* visible = */ false, /* stashed = */ false)
+ assertThat(mController.canShowRotationButton()).isFalse()
+
+ mController.onTaskbarStateChange( /* visible = */ true, /* stashed = */ false)
+
+ assertThat(mController.canShowRotationButton()).isTrue()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
index 11e502f..6cf1a12 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
@@ -47,7 +47,6 @@
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.battery.BatteryMeterViewController;
-import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.shade.NotificationPanelViewController;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
@@ -89,7 +88,9 @@
@Mock
private StatusBarIconController mStatusBarIconController;
@Mock
- private FeatureFlags mFeatureFlags;
+ private StatusBarIconController.TintedIconManager.Factory mIconManagerFactory;
+ @Mock
+ private StatusBarIconController.TintedIconManager mIconManager;
@Mock
private BatteryMeterViewController mBatteryMeterViewController;
@Mock
@@ -129,6 +130,8 @@
MockitoAnnotations.initMocks(this);
+ when(mIconManagerFactory.create(any())).thenReturn(mIconManager);
+
allowTestableLooperAsMainThread();
TestableLooper.get(this).runWithLooper(() -> {
mKeyguardStatusBarView =
@@ -148,7 +151,7 @@
mBatteryController,
mUserInfoController,
mStatusBarIconController,
- new StatusBarIconController.TintedIconManager.Factory(mFeatureFlags),
+ mIconManagerFactory,
mBatteryMeterViewController,
mNotificationPanelViewStateProvider,
mKeyguardStateController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
index 0f1c40b..a6b7e51 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
@@ -40,12 +40,16 @@
import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager;
import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState;
import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState;
+import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
+import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel;
import com.android.systemui.utils.leaks.LeakCheckedTest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import javax.inject.Provider;
+
@RunWith(AndroidTestingRunner.class)
@RunWithLooper
@SmallTest
@@ -67,7 +71,11 @@
@Test
public void testSetCalledOnAdd_DarkIconManager() {
LinearLayout layout = new LinearLayout(mContext);
- TestDarkIconManager manager = new TestDarkIconManager(layout, mock(FeatureFlags.class));
+ TestDarkIconManager manager = new TestDarkIconManager(
+ layout,
+ mock(FeatureFlags.class),
+ mock(StatusBarPipelineFlags.class),
+ () -> mock(WifiViewModel.class));
testCallOnAdd_forManager(manager);
}
@@ -104,8 +112,12 @@
private static class TestDarkIconManager extends DarkIconManager
implements TestableIconManager {
- TestDarkIconManager(LinearLayout group, FeatureFlags featureFlags) {
- super(group, featureFlags);
+ TestDarkIconManager(
+ LinearLayout group,
+ FeatureFlags featureFlags,
+ StatusBarPipelineFlags statusBarPipelineFlags,
+ Provider<WifiViewModel> wifiViewModelProvider) {
+ super(group, featureFlags, statusBarPipelineFlags, wifiViewModelProvider);
}
@Override
@@ -123,7 +135,7 @@
}
@Override
- protected StatusBarWifiView addSignalIcon(int index, String slot, WifiIconState state) {
+ protected StatusBarWifiView addWifiIcon(int index, String slot, WifiIconState state) {
StatusBarWifiView mock = mock(StatusBarWifiView.class);
mGroup.addView(mock, index);
return mock;
@@ -140,7 +152,10 @@
private static class TestIconManager extends IconManager implements TestableIconManager {
TestIconManager(ViewGroup group) {
- super(group, mock(FeatureFlags.class));
+ super(group,
+ mock(FeatureFlags.class),
+ mock(StatusBarPipelineFlags.class),
+ () -> mock(WifiViewModel.class));
}
@Override
@@ -158,7 +173,7 @@
}
@Override
- protected StatusBarWifiView addSignalIcon(int index, String slot, WifiIconState state) {
+ protected StatusBarWifiView addWifiIcon(int index, String slot, WifiIconState state) {
StatusBarWifiView mock = mock(StatusBarWifiView.class);
mGroup.addView(mock, index);
return mock;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
index de43a1f..2b80508 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
@@ -37,7 +37,6 @@
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
-import android.content.Intent;
import android.os.Handler;
import android.os.RemoteException;
import android.os.UserHandle;
@@ -135,8 +134,6 @@
@Mock
private PendingIntent mContentIntent;
@Mock
- private Intent mContentIntentInner;
- @Mock
private OnUserInteractionCallback mOnUserInteractionCallback;
@Mock
private Runnable mFutureDismissalRunnable;
@@ -159,7 +156,6 @@
MockitoAnnotations.initMocks(this);
when(mContentIntent.isActivity()).thenReturn(true);
when(mContentIntent.getCreatorUserHandle()).thenReturn(UserHandle.of(1));
- when(mContentIntent.getIntent()).thenReturn(mContentIntentInner);
NotificationTestHelper notificationTestHelper = new NotificationTestHelper(
mContext,
@@ -374,7 +370,6 @@
eq(entry.getKey()), any(NotificationVisibility.class));
// The content intent should NOT be sent on click.
- verify(mContentIntent).getIntent();
verify(mContentIntent).isActivity();
verifyNoMoreInteractions(mContentIntent);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 4c8599d..ceaceb4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -111,6 +111,10 @@
@Mock
private NotificationPanelViewController mNotificationPanelViewController;
@Mock
+ private StatusBarIconController.DarkIconManager.Factory mIconManagerFactory;
+ @Mock
+ private StatusBarIconController.DarkIconManager mIconManager;
+ @Mock
private StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
@Mock
private DumpManager mDumpManager;
@@ -424,6 +428,7 @@
mOperatorNameViewControllerFactory = mock(OperatorNameViewController.Factory.class);
when(mOperatorNameViewControllerFactory.create(any()))
.thenReturn(mOperatorNameViewController);
+ when(mIconManagerFactory.create(any())).thenReturn(mIconManager);
mSecureSettings = mock(SecureSettings.class);
setUpNotificationIconAreaController();
@@ -436,6 +441,7 @@
new PanelExpansionStateManager(),
mock(FeatureFlags.class),
mStatusBarIconController,
+ mIconManagerFactory,
mStatusBarHideIconsForBouncerManager,
mKeyguardStateController,
mNotificationPanelViewController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessorTest.kt
deleted file mode 100644
index 7b492cb..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessorTest.kt
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.pipeline
-
-import android.net.NetworkCapabilities
-import android.testing.AndroidTestingRunner
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.pipeline.wifi.data.repository.NetworkCapabilityInfo
-import com.android.systemui.util.mockito.mock
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.InternalCoroutinesApi
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.yield
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito.`when` as whenever
-
-@OptIn(InternalCoroutinesApi::class)
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-class ConnectivityInfoProcessorTest : SysuiTestCase() {
-
- private val statusBarPipelineFlags = mock<StatusBarPipelineFlags>()
-
- @Before
- fun setUp() {
- whenever(statusBarPipelineFlags.isNewPipelineEnabled()).thenReturn(true)
- }
-
- @Test
- fun collectorInfoUpdated_processedInfoAlsoUpdated() = runBlocking {
- // GIVEN a processor hooked up to a collector
- val scope = CoroutineScope(Dispatchers.Unconfined)
- val collector = FakeConnectivityInfoCollector()
- val processor = ConnectivityInfoProcessor(
- collector,
- context,
- scope,
- statusBarPipelineFlags,
- mock(),
- )
-
- var mostRecentValue: ProcessedConnectivityInfo? = null
- val job = launch(start = CoroutineStart.UNDISPATCHED) {
- processor.processedInfoFlow.collect {
- mostRecentValue = it
- }
- }
-
- // WHEN the collector emits a value
- val networkCapabilityInfo = mapOf(
- 10 to NetworkCapabilityInfo(mock(), NetworkCapabilities.Builder().build())
- )
- collector.emitValue(RawConnectivityInfo(networkCapabilityInfo))
- // Because our job uses [CoroutineStart.UNDISPATCHED], it executes in the same thread as
- // this test. So, our test needs to yield to let the job run.
- // Note: Once we upgrade our Kotlin coroutines testing library, we won't need this.
- yield()
-
- // THEN the processor receives it
- assertThat(mostRecentValue?.networkCapabilityInfo).isEqualTo(networkCapabilityInfo)
-
- job.cancel()
- scope.cancel()
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/FakeConnectivityInfoCollector.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/FakeConnectivityInfoCollector.kt
deleted file mode 100644
index 710e5f6..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/FakeConnectivityInfoCollector.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.pipeline
-
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-
-/**
- * A test-friendly implementation of [ConnectivityInfoCollector] that just emits whatever value it
- * receives in [emitValue].
- */
-class FakeConnectivityInfoCollector : ConnectivityInfoCollector {
- private val _rawConnectivityInfoFlow = MutableStateFlow(RawConnectivityInfo())
- override val rawConnectivityInfoFlow = _rawConnectivityInfoFlow.asStateFlow()
-
- suspend fun emitValue(value: RawConnectivityInfo) {
- _rawConnectivityInfoFlow.emit(value)
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
index df389bc..6b8d4aa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
@@ -17,21 +17,22 @@
package com.android.systemui.statusbar.pipeline.wifi.data.repository
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel
-import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.ACTIVITY_DEFAULT
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
/** Fake implementation of [WifiRepository] exposing set methods for all the flows. */
class FakeWifiRepository : WifiRepository {
- private val _wifiModel: MutableStateFlow<WifiModel?> = MutableStateFlow(null)
- override val wifiModel: Flow<WifiModel?> = _wifiModel
+ private val _wifiNetwork: MutableStateFlow<WifiNetworkModel> =
+ MutableStateFlow(WifiNetworkModel.Inactive)
+ override val wifiNetwork: Flow<WifiNetworkModel> = _wifiNetwork
private val _wifiActivity = MutableStateFlow(ACTIVITY_DEFAULT)
override val wifiActivity: Flow<WifiActivityModel> = _wifiActivity
- fun setWifiModel(wifiModel: WifiModel?) {
- _wifiModel.value = wifiModel
+ fun setWifiNetwork(wifiNetworkModel: WifiNetworkModel) {
+ _wifiNetwork.value = wifiNetworkModel
}
fun setWifiActivity(activity: WifiActivityModel) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepoTest.kt
deleted file mode 100644
index 6edf76c..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepoTest.kt
+++ /dev/null
@@ -1,252 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.pipeline.wifi.data.repository
-
-import android.net.ConnectivityManager
-import android.net.ConnectivityManager.NetworkCallback
-import android.net.Network
-import android.net.NetworkCapabilities
-import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED
-import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
-import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
-import android.net.NetworkCapabilities.TRANSPORT_WIFI
-import android.net.NetworkRequest
-import android.test.suitebuilder.annotation.SmallTest
-import android.testing.AndroidTestingRunner
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.withArgCaptor
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.any
-import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
-import org.mockito.MockitoAnnotations
-
-// TODO(b/240619365): Update this test to use `runTest` when we update the testing library
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-class NetworkCapabilitiesRepoTest : SysuiTestCase() {
- @Mock private lateinit var connectivityManager: ConnectivityManager
- @Mock private lateinit var logger: ConnectivityPipelineLogger
-
- @Before
- fun setup() {
- MockitoAnnotations.initMocks(this)
- }
-
- @Test
- fun testOnCapabilitiesChanged_oneNewNetwork_networkStored() = runBlocking {
- // GIVEN a repo hooked up to [ConnectivityManager]
- val scope = CoroutineScope(Dispatchers.Unconfined)
- val repo = NetworkCapabilitiesRepo(
- connectivityManager = connectivityManager,
- scope = scope,
- logger = logger,
- )
-
- val job = launch(start = CoroutineStart.UNDISPATCHED) {
- repo.dataStream.collect {
- }
- }
-
- val callback: NetworkCallback = withArgCaptor {
- verify(connectivityManager)
- .registerNetworkCallback(any(NetworkRequest::class.java), capture())
- }
-
- // WHEN a new network is added
- callback.onCapabilitiesChanged(NET_1, NET_1_CAPS)
-
- val currentMap = repo.dataStream.value
-
- // THEN it is emitted from the flow
- assertThat(currentMap[NET_1_ID]?.network).isEqualTo(NET_1)
- assertThat(currentMap[NET_1_ID]?.capabilities).isEqualTo(NET_1_CAPS)
-
- job.cancel()
- scope.cancel()
- }
-
- @Test
- fun testOnCapabilitiesChanged_twoNewNetworks_bothStored() = runBlocking {
- // GIVEN a repo hooked up to [ConnectivityManager]
- val scope = CoroutineScope(Dispatchers.Unconfined)
- val repo = NetworkCapabilitiesRepo(
- connectivityManager = connectivityManager,
- scope = scope,
- logger = logger,
- )
-
- val job = launch(start = CoroutineStart.UNDISPATCHED) {
- repo.dataStream.collect {
- }
- }
-
- val callback: NetworkCallback = withArgCaptor {
- verify(connectivityManager)
- .registerNetworkCallback(any(NetworkRequest::class.java), capture())
- }
-
- // WHEN two new networks are added
- callback.onCapabilitiesChanged(NET_1, NET_1_CAPS)
- callback.onCapabilitiesChanged(NET_2, NET_2_CAPS)
-
- val currentMap = repo.dataStream.value
-
- // THEN the current state of the flow reflects 2 networks
- assertThat(currentMap[NET_1_ID]?.network).isEqualTo(NET_1)
- assertThat(currentMap[NET_1_ID]?.capabilities).isEqualTo(NET_1_CAPS)
- assertThat(currentMap[NET_2_ID]?.network).isEqualTo(NET_2)
- assertThat(currentMap[NET_2_ID]?.capabilities).isEqualTo(NET_2_CAPS)
-
- job.cancel()
- scope.cancel()
- }
-
- @Test
- fun testOnCapabilitesChanged_newCapabilitiesForExistingNetwork_areCaptured() = runBlocking {
- // GIVEN a repo hooked up to [ConnectivityManager]
- val scope = CoroutineScope(Dispatchers.Unconfined)
- val repo = NetworkCapabilitiesRepo(
- connectivityManager = connectivityManager,
- scope = scope,
- logger = logger,
- )
-
- val job = launch(start = CoroutineStart.UNDISPATCHED) {
- repo.dataStream.collect {
- }
- }
-
- val callback: NetworkCallback = withArgCaptor {
- verify(connectivityManager)
- .registerNetworkCallback(any(NetworkRequest::class.java), capture())
- }
-
- // WHEN a network is added, and then its capabilities are changed
- callback.onCapabilitiesChanged(NET_1, NET_1_CAPS)
- callback.onCapabilitiesChanged(NET_1, NET_2_CAPS)
-
- val currentMap = repo.dataStream.value
-
- // THEN the current state of the flow reflects the new capabilities
- assertThat(currentMap[NET_1_ID]?.capabilities).isEqualTo(NET_2_CAPS)
-
- job.cancel()
- scope.cancel()
- }
-
- @Test
- fun testOnLost_networkIsRemoved() = runBlocking {
- // GIVEN a repo hooked up to [ConnectivityManager]
- val scope = CoroutineScope(Dispatchers.Unconfined)
- val repo = NetworkCapabilitiesRepo(
- connectivityManager = connectivityManager,
- scope = scope,
- logger = logger,
- )
-
- val job = launch(start = CoroutineStart.UNDISPATCHED) {
- repo.dataStream.collect {
- }
- }
-
- val callback: NetworkCallback = withArgCaptor {
- verify(connectivityManager)
- .registerNetworkCallback(any(NetworkRequest::class.java), capture())
- }
-
- // WHEN two new networks are added, and one is removed
- callback.onCapabilitiesChanged(NET_1, NET_1_CAPS)
- callback.onCapabilitiesChanged(NET_2, NET_2_CAPS)
- callback.onLost(NET_1)
-
- val currentMap = repo.dataStream.value
-
- // THEN the current state of the flow reflects only the remaining network
- assertThat(currentMap[NET_1_ID]).isNull()
- assertThat(currentMap[NET_2_ID]?.network).isEqualTo(NET_2)
- assertThat(currentMap[NET_2_ID]?.capabilities).isEqualTo(NET_2_CAPS)
-
- job.cancel()
- scope.cancel()
- }
-
- @Test
- fun testOnLost_noNetworks_doesNotCrash() = runBlocking {
- // GIVEN a repo hooked up to [ConnectivityManager]
- val scope = CoroutineScope(Dispatchers.Unconfined)
- val repo = NetworkCapabilitiesRepo(
- connectivityManager = connectivityManager,
- scope = scope,
- logger = logger,
- )
-
- val job = launch(start = CoroutineStart.UNDISPATCHED) {
- repo.dataStream.collect {
- }
- }
-
- val callback: NetworkCallback = withArgCaptor {
- verify(connectivityManager)
- .registerNetworkCallback(any(NetworkRequest::class.java), capture())
- }
-
- // WHEN no networks are added, and one is removed
- callback.onLost(NET_1)
-
- val currentMap = repo.dataStream.value
-
- // THEN the current state of the flow shows no networks
- assertThat(currentMap).isEmpty()
-
- job.cancel()
- scope.cancel()
- }
-
- private val NET_1_ID = 100
- private val NET_1 = mock<Network>().also {
- whenever(it.getNetId()).thenReturn(NET_1_ID)
- }
- private val NET_2_ID = 200
- private val NET_2 = mock<Network>().also {
- whenever(it.getNetId()).thenReturn(NET_2_ID)
- }
-
- private val NET_1_CAPS = NetworkCapabilities.Builder()
- .addTransportType(TRANSPORT_CELLULAR)
- .addCapability(NET_CAPABILITY_VALIDATED)
- .build()
-
- private val NET_2_CAPS = NetworkCapabilities.Builder()
- .addTransportType(TRANSPORT_WIFI)
- .addCapability(NET_CAPABILITY_NOT_METERED)
- .addCapability(NET_CAPABILITY_VALIDATED)
- .build()
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
index 8b61364..d0a3808 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
@@ -16,16 +16,26 @@
package com.android.systemui.statusbar.pipeline.wifi.data.repository
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.vcn.VcnTransportInfo
+import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
import android.net.wifi.WifiManager.TrafficStateCallback
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.ACTIVITY_DEFAULT
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.WIFI_NETWORK_DEFAULT
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.mock
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.Executor
@@ -38,6 +48,7 @@
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@@ -47,6 +58,7 @@
private lateinit var underTest: WifiRepositoryImpl
@Mock private lateinit var logger: ConnectivityPipelineLogger
+ @Mock private lateinit var connectivityManager: ConnectivityManager
@Mock private lateinit var wifiManager: WifiManager
private lateinit var executor: Executor
@@ -54,11 +66,347 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
executor = FakeExecutor(FakeSystemClock())
+
+ underTest = WifiRepositoryImpl(
+ connectivityManager,
+ wifiManager,
+ executor,
+ logger,
+ )
+ }
+
+ @Test
+ fun wifiNetwork_initiallyGetsDefault() = runBlocking(IMMEDIATE) {
+ var latest: WifiNetworkModel? = null
+ val job = underTest
+ .wifiNetwork
+ .onEach { latest = it }
+ .launchIn(this)
+
+ assertThat(latest).isEqualTo(WIFI_NETWORK_DEFAULT)
+
+ job.cancel()
+ }
+
+ @Test
+ fun wifiNetwork_primaryWifiNetworkAdded_flowHasNetwork() = runBlocking(IMMEDIATE) {
+ var latest: WifiNetworkModel? = null
+ val job = underTest
+ .wifiNetwork
+ .onEach { latest = it }
+ .launchIn(this)
+
+ val wifiInfo = mock<WifiInfo>().apply {
+ whenever(this.ssid).thenReturn(SSID)
+ whenever(this.isPrimary).thenReturn(true)
+ }
+ val network = mock<Network>().apply {
+ whenever(this.getNetId()).thenReturn(NETWORK_ID)
+ }
+
+ getNetworkCallback().onCapabilitiesChanged(network, createWifiNetworkCapabilities(wifiInfo))
+
+ assertThat(latest is WifiNetworkModel.Active).isTrue()
+ val latestActive = latest as WifiNetworkModel.Active
+ assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
+ assertThat(latestActive.ssid).isEqualTo(SSID)
+
+ job.cancel()
+ }
+
+ @Test
+ fun wifiNetwork_nonPrimaryWifiNetworkAdded_flowHasNoNetwork() = runBlocking(IMMEDIATE) {
+ var latest: WifiNetworkModel? = null
+ val job = underTest
+ .wifiNetwork
+ .onEach { latest = it }
+ .launchIn(this)
+
+ val wifiInfo = mock<WifiInfo>().apply {
+ whenever(this.ssid).thenReturn(SSID)
+ whenever(this.isPrimary).thenReturn(false)
+ }
+
+ getNetworkCallback().onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo))
+
+ assertThat(latest is WifiNetworkModel.Inactive).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun wifiNetwork_cellularVcnNetworkAdded_flowHasNetwork() = runBlocking(IMMEDIATE) {
+ var latest: WifiNetworkModel? = null
+ val job = underTest
+ .wifiNetwork
+ .onEach { latest = it }
+ .launchIn(this)
+
+ val capabilities = mock<NetworkCapabilities>().apply {
+ whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
+ whenever(this.transportInfo).thenReturn(VcnTransportInfo(PRIMARY_WIFI_INFO))
+ }
+
+ getNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
+
+ assertThat(latest is WifiNetworkModel.Active).isTrue()
+ val latestActive = latest as WifiNetworkModel.Active
+ assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
+ assertThat(latestActive.ssid).isEqualTo(SSID)
+
+ job.cancel()
+ }
+
+ @Test
+ fun wifiNetwork_nonPrimaryCellularVcnNetworkAdded_flowHasNoNetwork() = runBlocking(IMMEDIATE) {
+ var latest: WifiNetworkModel? = null
+ val job = underTest
+ .wifiNetwork
+ .onEach { latest = it }
+ .launchIn(this)
+
+ val wifiInfo = mock<WifiInfo>().apply {
+ whenever(this.ssid).thenReturn(SSID)
+ whenever(this.isPrimary).thenReturn(false)
+ }
+ val capabilities = mock<NetworkCapabilities>().apply {
+ whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
+ whenever(this.transportInfo).thenReturn(VcnTransportInfo(wifiInfo))
+ }
+
+ getNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
+
+ assertThat(latest is WifiNetworkModel.Inactive).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun wifiNetwork_cellularNotVcnNetworkAdded_flowHasNoNetwork() = runBlocking(IMMEDIATE) {
+ var latest: WifiNetworkModel? = null
+ val job = underTest
+ .wifiNetwork
+ .onEach { latest = it }
+ .launchIn(this)
+
+ val capabilities = mock<NetworkCapabilities>().apply {
+ whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
+ whenever(this.transportInfo).thenReturn(mock())
+ }
+
+ getNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
+
+ assertThat(latest is WifiNetworkModel.Inactive).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun wifiNetwork_newPrimaryWifiNetwork_flowHasNewNetwork() = runBlocking(IMMEDIATE) {
+ var latest: WifiNetworkModel? = null
+ val job = underTest
+ .wifiNetwork
+ .onEach { latest = it }
+ .launchIn(this)
+
+ // Start with the original network
+ getNetworkCallback()
+ .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+
+ // WHEN we update to a new primary network
+ val newNetworkId = 456
+ val newNetwork = mock<Network>().apply {
+ whenever(this.getNetId()).thenReturn(newNetworkId)
+ }
+ val newSsid = "CD"
+ val newWifiInfo = mock<WifiInfo>().apply {
+ whenever(this.ssid).thenReturn(newSsid)
+ whenever(this.isPrimary).thenReturn(true)
+ }
+
+ getNetworkCallback().onCapabilitiesChanged(
+ newNetwork, createWifiNetworkCapabilities(newWifiInfo)
+ )
+
+ // THEN we use the new network
+ assertThat(latest is WifiNetworkModel.Active).isTrue()
+ val latestActive = latest as WifiNetworkModel.Active
+ assertThat(latestActive.networkId).isEqualTo(newNetworkId)
+ assertThat(latestActive.ssid).isEqualTo(newSsid)
+
+ job.cancel()
+ }
+
+ @Test
+ fun wifiNetwork_newNonPrimaryWifiNetwork_flowHasOldNetwork() = runBlocking(IMMEDIATE) {
+ var latest: WifiNetworkModel? = null
+ val job = underTest
+ .wifiNetwork
+ .onEach { latest = it }
+ .launchIn(this)
+
+ // Start with the original network
+ getNetworkCallback()
+ .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+
+ // WHEN we notify of a new but non-primary network
+ val newNetworkId = 456
+ val newNetwork = mock<Network>().apply {
+ whenever(this.getNetId()).thenReturn(newNetworkId)
+ }
+ val newSsid = "EF"
+ val newWifiInfo = mock<WifiInfo>().apply {
+ whenever(this.ssid).thenReturn(newSsid)
+ whenever(this.isPrimary).thenReturn(false)
+ }
+
+ getNetworkCallback().onCapabilitiesChanged(
+ newNetwork, createWifiNetworkCapabilities(newWifiInfo)
+ )
+
+ // THEN we still use the original network
+ assertThat(latest is WifiNetworkModel.Active).isTrue()
+ val latestActive = latest as WifiNetworkModel.Active
+ assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
+ assertThat(latestActive.ssid).isEqualTo(SSID)
+
+ job.cancel()
+ }
+
+ @Test
+ fun wifiNetwork_newNetworkCapabilities_flowHasNewData() = runBlocking(IMMEDIATE) {
+ var latest: WifiNetworkModel? = null
+ val job = underTest
+ .wifiNetwork
+ .onEach { latest = it }
+ .launchIn(this)
+
+ val wifiInfo = mock<WifiInfo>().apply {
+ whenever(this.ssid).thenReturn(SSID)
+ whenever(this.isPrimary).thenReturn(true)
+ }
+
+ // Start with the original network
+ getNetworkCallback()
+ .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo))
+
+ // WHEN we keep the same network ID but change the SSID
+ val newSsid = "CD"
+ val newWifiInfo = mock<WifiInfo>().apply {
+ whenever(this.ssid).thenReturn(newSsid)
+ whenever(this.isPrimary).thenReturn(true)
+ }
+
+ getNetworkCallback()
+ .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(newWifiInfo))
+
+ // THEN we've updated to the new SSID
+ assertThat(latest is WifiNetworkModel.Active).isTrue()
+ val latestActive = latest as WifiNetworkModel.Active
+ assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
+ assertThat(latestActive.ssid).isEqualTo(newSsid)
+
+ job.cancel()
+ }
+
+ @Test
+ fun wifiNetwork_noCurrentNetwork_networkLost_flowHasNoNetwork() = runBlocking(IMMEDIATE) {
+ var latest: WifiNetworkModel? = null
+ val job = underTest
+ .wifiNetwork
+ .onEach { latest = it }
+ .launchIn(this)
+
+ // WHEN we receive #onLost without any #onCapabilitiesChanged beforehand
+ getNetworkCallback().onLost(NETWORK)
+
+ // THEN there's no crash and we still have no network
+ assertThat(latest is WifiNetworkModel.Inactive).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun wifiNetwork_currentNetworkLost_flowHasNoNetwork() = runBlocking(IMMEDIATE) {
+ var latest: WifiNetworkModel? = null
+ val job = underTest
+ .wifiNetwork
+ .onEach { latest = it }
+ .launchIn(this)
+
+ getNetworkCallback()
+ .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+ assertThat((latest as WifiNetworkModel.Active).networkId).isEqualTo(NETWORK_ID)
+
+ // WHEN we lose our current network
+ getNetworkCallback().onLost(NETWORK)
+
+ // THEN we update to no network
+ assertThat(latest is WifiNetworkModel.Inactive).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun wifiNetwork_unknownNetworkLost_flowHasPreviousNetwork() = runBlocking(IMMEDIATE) {
+ var latest: WifiNetworkModel? = null
+ val job = underTest
+ .wifiNetwork
+ .onEach { latest = it }
+ .launchIn(this)
+
+ getNetworkCallback()
+ .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+ assertThat((latest as WifiNetworkModel.Active).networkId).isEqualTo(NETWORK_ID)
+
+ // WHEN we lose an unknown network
+ val unknownNetwork = mock<Network>().apply {
+ whenever(this.getNetId()).thenReturn(543)
+ }
+ getNetworkCallback().onLost(unknownNetwork)
+
+ // THEN we still have our previous network
+ assertThat(latest is WifiNetworkModel.Active).isTrue()
+ val latestActive = latest as WifiNetworkModel.Active
+ assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
+ assertThat(latestActive.ssid).isEqualTo(SSID)
+
+ job.cancel()
+ }
+
+ @Test
+ fun wifiNetwork_notCurrentNetworkLost_flowHasCurrentNetwork() = runBlocking(IMMEDIATE) {
+ var latest: WifiNetworkModel? = null
+ val job = underTest
+ .wifiNetwork
+ .onEach { latest = it }
+ .launchIn(this)
+
+ getNetworkCallback()
+ .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+ assertThat((latest as WifiNetworkModel.Active).networkId).isEqualTo(NETWORK_ID)
+
+ // WHEN we update to a new network...
+ val newNetworkId = 89
+ val newNetwork = mock<Network>().apply {
+ whenever(this.getNetId()).thenReturn(newNetworkId)
+ }
+ getNetworkCallback().onCapabilitiesChanged(
+ newNetwork, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO)
+ )
+ // ...and lose the old network
+ getNetworkCallback().onLost(NETWORK)
+
+ // THEN we still have the new network
+ assertThat((latest as WifiNetworkModel.Active).networkId).isEqualTo(newNetworkId)
+
+ job.cancel()
}
@Test
fun wifiActivity_nullWifiManager_receivesDefault() = runBlocking(IMMEDIATE) {
underTest = WifiRepositoryImpl(
+ connectivityManager,
wifiManager = null,
executor,
logger,
@@ -77,12 +425,6 @@
@Test
fun wifiActivity_callbackGivesNone_activityFlowHasNone() = runBlocking(IMMEDIATE) {
- underTest = WifiRepositoryImpl(
- wifiManager,
- executor,
- logger,
- )
-
var latest: WifiActivityModel? = null
val job = underTest
.wifiActivity
@@ -100,12 +442,6 @@
@Test
fun wifiActivity_callbackGivesIn_activityFlowHasIn() = runBlocking(IMMEDIATE) {
- underTest = WifiRepositoryImpl(
- wifiManager,
- executor,
- logger,
- )
-
var latest: WifiActivityModel? = null
val job = underTest
.wifiActivity
@@ -123,12 +459,6 @@
@Test
fun wifiActivity_callbackGivesOut_activityFlowHasOut() = runBlocking(IMMEDIATE) {
- underTest = WifiRepositoryImpl(
- wifiManager,
- executor,
- logger,
- )
-
var latest: WifiActivityModel? = null
val job = underTest
.wifiActivity
@@ -146,12 +476,6 @@
@Test
fun wifiActivity_callbackGivesInout_activityFlowHasInAndOut() = runBlocking(IMMEDIATE) {
- underTest = WifiRepositoryImpl(
- wifiManager,
- executor,
- logger,
- )
-
var latest: WifiActivityModel? = null
val job = underTest
.wifiActivity
@@ -170,6 +494,30 @@
verify(wifiManager).registerTrafficStateCallback(any(), callbackCaptor.capture())
return callbackCaptor.value!!
}
+
+ private fun getNetworkCallback(): ConnectivityManager.NetworkCallback {
+ val callbackCaptor = argumentCaptor<ConnectivityManager.NetworkCallback>()
+ verify(connectivityManager).registerNetworkCallback(any(), callbackCaptor.capture())
+ return callbackCaptor.value!!
+ }
+
+ private fun createWifiNetworkCapabilities(wifiInfo: WifiInfo) =
+ mock<NetworkCapabilities>().apply {
+ whenever(this.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
+ whenever(this.transportInfo).thenReturn(wifiInfo)
+ }
+
+ private companion object {
+ const val NETWORK_ID = 45
+ val NETWORK = mock<Network>().apply {
+ whenever(this.getNetId()).thenReturn(NETWORK_ID)
+ }
+ const val SSID = "AB"
+ val PRIMARY_WIFI_INFO: WifiInfo = mock<WifiInfo>().apply {
+ whenever(this.ssid).thenReturn(SSID)
+ whenever(this.isPrimary).thenReturn(true)
+ }
+ }
}
private val IMMEDIATE = Dispatchers.Main.immediate
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt
index c52f347..5f1b1db 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt
@@ -19,7 +19,7 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel
-import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
@@ -47,7 +47,7 @@
@Test
fun hasActivityIn_noInOrOut_outputsFalse() = runBlocking(IMMEDIATE) {
- repository.setWifiModel(WifiModel(ssid = "AB"))
+ repository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
repository.setWifiActivity(WifiActivityModel(hasActivityIn = false, hasActivityOut = false))
var latest: Boolean? = null
@@ -63,7 +63,7 @@
@Test
fun hasActivityIn_onlyOut_outputsFalse() = runBlocking(IMMEDIATE) {
- repository.setWifiModel(WifiModel(ssid = "AB"))
+ repository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
repository.setWifiActivity(WifiActivityModel(hasActivityIn = false, hasActivityOut = true))
var latest: Boolean? = null
@@ -79,7 +79,7 @@
@Test
fun hasActivityIn_onlyIn_outputsTrue() = runBlocking(IMMEDIATE) {
- repository.setWifiModel(WifiModel(ssid = "AB"))
+ repository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = false))
var latest: Boolean? = null
@@ -95,7 +95,7 @@
@Test
fun hasActivityIn_inAndOut_outputsTrue() = runBlocking(IMMEDIATE) {
- repository.setWifiModel(WifiModel(ssid = "AB"))
+ repository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = true))
var latest: Boolean? = null
@@ -111,7 +111,7 @@
@Test
fun hasActivityIn_ssidNull_outputsFalse() = runBlocking(IMMEDIATE) {
- repository.setWifiModel(WifiModel(ssid = null))
+ repository.setWifiNetwork(WifiNetworkModel.Active(networkId = 1, ssid = null))
repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = true))
var latest: Boolean? = null
@@ -127,7 +127,7 @@
@Test
fun hasActivityIn_multipleChanges_multipleOutputChanges() = runBlocking(IMMEDIATE) {
- repository.setWifiModel(WifiModel(ssid = "AB"))
+ repository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
var latest: Boolean? = null
val job = underTest
@@ -158,6 +158,10 @@
job.cancel()
}
+
+ companion object {
+ val VALID_WIFI_NETWORK_MODEL = WifiNetworkModel.Active(networkId = 1, ssid = "AB")
+ }
}
private val IMMEDIATE = Dispatchers.Main.immediate
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
new file mode 100644
index 0000000..3c200a5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.wifi.ui.view
+
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.lifecycle.InstantTaskExecutorRule
+import com.android.systemui.util.Assert
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+@RunWithLooper
+class ModernStatusBarWifiViewTest : SysuiTestCase() {
+
+ @JvmField @Rule
+ val instantTaskExecutor = InstantTaskExecutorRule()
+
+ @Before
+ fun setUp() {
+ Assert.setTestThread(Thread.currentThread())
+ }
+
+ @Test
+ fun constructAndBind_hasCorrectSlot() {
+ val view = ModernStatusBarWifiView.constructAndBind(
+ context, "slotName", mock()
+ )
+
+ assertThat(view.slot).isEqualTo("slotName")
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
index e9259b0..c790734 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
@@ -18,9 +18,10 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel
-import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
@@ -43,6 +44,7 @@
private lateinit var underTest: WifiViewModel
+ @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
@Mock private lateinit var logger: ConnectivityPipelineLogger
@Mock private lateinit var constants: WifiConstants
private lateinit var repository: FakeWifiRepository
@@ -55,13 +57,14 @@
interactor = WifiInteractor(repository)
underTest = WifiViewModel(
- constants,
- logger,
- interactor
+ statusBarPipelineFlags,
+ constants,
+ logger,
+ interactor
)
// Set up with a valid SSID
- repository.setWifiModel(WifiModel(ssid = "AB"))
+ repository.setWifiNetwork(WifiNetworkModel.Active(networkId = 1, ssid = "AB"))
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/IpcSerializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/IpcSerializerTest.kt
new file mode 100644
index 0000000..15ba672
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/IpcSerializerTest.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.kotlin
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import java.util.concurrent.atomic.AtomicLong
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class IpcSerializerTest : SysuiTestCase() {
+
+ private val serializer = IpcSerializer()
+
+ @Test
+ fun serializeManyIncomingIpcs(): Unit = runBlocking(Dispatchers.Main.immediate) {
+ val processor = launch(start = CoroutineStart.LAZY) { serializer.process() }
+ withContext(Dispatchers.IO) {
+ val lastEvaluatedTime = AtomicLong(System.currentTimeMillis())
+ // First, launch many serialization requests in parallel
+ repeat(100_000) {
+ launch(Dispatchers.Unconfined) {
+ val enqueuedTime = System.currentTimeMillis()
+ serializer.runSerialized {
+ val last = lastEvaluatedTime.getAndSet(enqueuedTime)
+ assertTrue(
+ "expected $last less than or equal to $enqueuedTime ",
+ last <= enqueuedTime,
+ )
+ }
+ }
+ }
+ // Then, process them all in the order they came in.
+ processor.start()
+ }
+ // All done, stop processing
+ processor.cancel()
+ }
+
+ @Test(timeout = 5000)
+ fun serializeOnOneThread_doesNotDeadlock() = runBlocking {
+ val job = launch { serializer.process() }
+ repeat(100) {
+ serializer.runSerializedBlocking { }
+ }
+ job.cancel()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 8f2b715..5d63632 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -135,6 +135,7 @@
import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.onehanded.OneHandedController;
+import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
@@ -225,6 +226,8 @@
@Mock
private ShellInit mShellInit;
@Mock
+ private ShellCommandHandler mShellCommandHandler;
+ @Mock
private ShellController mShellController;
@Mock
private Bubbles.BubbleExpandListener mBubbleExpandListener;
@@ -349,6 +352,7 @@
mBubbleController = new TestableBubbleController(
mContext,
mShellInit,
+ mShellCommandHandler,
mShellController,
mBubbleData,
mFloatingContentCoordinator,
@@ -389,7 +393,6 @@
mCommonNotifCollection,
mNotifPipeline,
mSysUiState,
- mDumpManager,
syncExecutor);
mBubblesManager.addNotifCallback(mNotifCallback);
@@ -1395,6 +1398,33 @@
assertThat(stackView.getVisibility()).isEqualTo(View.VISIBLE);
}
+ /**
+ * Test to verify behavior for following situation:
+ * <ul>
+ * <li>status bar shade state is set to <code>false</code></li>
+ * <li>there is a bubble pending to be expanded</li>
+ * </ul>
+ * Test that duplicate status bar state updates to <code>false</code> do not clear the
+ * pending bubble to be
+ * expanded.
+ */
+ @Test
+ public void testOnStatusBarStateChanged_statusBarChangeDoesNotClearExpandingBubble() {
+ mBubbleController.updateBubble(mBubbleEntry);
+ mBubbleController.onStatusBarStateChanged(false);
+ // Set the bubble to expand once status bar state changes
+ mBubbleController.expandStackAndSelectBubble(mBubbleEntry);
+ // Check that stack is currently collapsed
+ assertStackCollapsed();
+ // Post status bar state change update with the same value
+ mBubbleController.onStatusBarStateChanged(false);
+ // Stack should remain collapsedb
+ assertStackCollapsed();
+ // Post status bar state change which should trigger bubble to expand
+ mBubbleController.onStatusBarStateChanged(true);
+ assertStackExpanded();
+ }
+
@Test
public void testSetShouldAutoExpand_notifiesFlagChanged() {
mBubbleController.updateBubble(mBubbleEntry);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
index 880ad187..6357a09 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
@@ -38,6 +38,7 @@
import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.onehanded.OneHandedController;
+import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
@@ -51,6 +52,7 @@
// Let's assume surfaces can be synchronized immediately.
TestableBubbleController(Context context,
ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
ShellController shellController,
BubbleData data,
FloatingContentCoordinator floatingContentCoordinator,
@@ -71,12 +73,12 @@
Handler shellMainHandler,
TaskViewTransitions taskViewTransitions,
SyncTransactionQueue syncQueue) {
- super(context, shellInit, shellController, data, Runnable::run, floatingContentCoordinator,
- dataRepository, statusBarService, windowManager, windowManagerShellWrapper,
- userManager, launcherApps, bubbleLogger, taskStackListener, shellTaskOrganizer,
- positioner, displayController, oneHandedOptional, dragAndDropController,
- shellMainExecutor, shellMainHandler, new SyncExecutor(), taskViewTransitions,
- syncQueue);
+ super(context, shellInit, shellCommandHandler, shellController, data, Runnable::run,
+ floatingContentCoordinator, dataRepository, statusBarService, windowManager,
+ windowManagerShellWrapper, userManager, launcherApps, bubbleLogger,
+ taskStackListener, shellTaskOrganizer, positioner, displayController,
+ oneHandedOptional, dragAndDropController, shellMainExecutor, shellMainHandler,
+ new SyncExecutor(), taskViewTransitions, syncQueue);
setInflateSynchronously(true);
onInit();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
index 9c21366..da33fa6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
@@ -28,10 +28,10 @@
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.model.SysUiState;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.statusbar.policy.UserInfoController;
import com.android.systemui.tracing.ProtoTracer;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.onehanded.OneHanded;
@@ -72,7 +72,7 @@
@Mock OneHanded mOneHanded;
@Mock WakefulnessLifecycle mWakefulnessLifecycle;
@Mock ProtoTracer mProtoTracer;
- @Mock UserInfoController mUserInfoController;
+ @Mock UserTracker mUserTracker;
@Mock ShellExecutor mSysUiMainExecutor;
@Before
@@ -83,7 +83,7 @@
Optional.of(mSplitScreen), Optional.of(mOneHanded), mCommandQueue,
mConfigurationController, mKeyguardStateController, mKeyguardUpdateMonitor,
mScreenLifecycle, mSysUiState, mProtoTracer, mWakefulnessLifecycle,
- mUserInfoController, mSysUiMainExecutor);
+ mUserTracker, mSysUiMainExecutor);
}
@Test
diff --git a/services/OWNERS b/services/OWNERS
index 67cee55..495c0737 100644
--- a/services/OWNERS
+++ b/services/OWNERS
@@ -1,4 +1,4 @@
-per-file Android.bp = file:platform/build/soong:/OWNERS
+per-file Android.bp = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION}
# art-team@ manages the system server profile
per-file art-profile* = calin@google.com, ngeoffray@google.com, vmarko@google.com
diff --git a/services/api/OWNERS b/services/api/OWNERS
index a609390..e10440c 100644
--- a/services/api/OWNERS
+++ b/services/api/OWNERS
@@ -1,4 +1,4 @@
-per-file Android.bp = file:platform/build/soong:/OWNERS
+per-file Android.bp = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION}
# API changes are managed via Prolog rules, not OWNERS
*
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index 0b858cf..eb1d2d7 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -2987,8 +2987,8 @@
pw.println("mBarringInfo=" + mBarringInfo.get(i));
pw.println("mCarrierNetworkChangeState=" + mCarrierNetworkChangeState[i]);
pw.println("mTelephonyDisplayInfo=" + mTelephonyDisplayInfos[i]);
- pw.println("mIsDataEnabled=" + mIsDataEnabled);
- pw.println("mDataEnabledReason=" + mDataEnabledReason);
+ pw.println("mIsDataEnabled=" + mIsDataEnabled[i]);
+ pw.println("mDataEnabledReason=" + mDataEnabledReason[i]);
pw.println("mAllowedNetworkTypeReason=" + mAllowedNetworkTypeReason[i]);
pw.println("mAllowedNetworkTypeValue=" + mAllowedNetworkTypeValue[i]);
pw.println("mPhysicalChannelConfigs=" + mPhysicalChannelConfigs.get(i));
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 297e6a2..734f455 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -454,6 +454,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -5501,7 +5502,7 @@
IIntentSender pendingResult, int matchFlags) {
enforceCallingPermission(Manifest.permission.GET_INTENT_SENDER_INTENT,
"queryIntentComponentsForIntentSender()");
- Preconditions.checkNotNull(pendingResult);
+ Objects.requireNonNull(pendingResult);
final PendingIntentRecord res;
try {
res = (PendingIntentRecord) pendingResult;
@@ -5513,17 +5514,19 @@
return null;
}
final int userId = res.key.userId;
+ final int uid = res.uid;
+ final String resolvedType = res.key.requestResolvedType;
switch (res.key.type) {
case ActivityManager.INTENT_SENDER_ACTIVITY:
- return new ParceledListSlice<>(mContext.getPackageManager()
- .queryIntentActivitiesAsUser(intent, matchFlags, userId));
+ return new ParceledListSlice<>(mPackageManagerInt.queryIntentActivities(
+ intent, resolvedType, matchFlags, uid, userId));
case ActivityManager.INTENT_SENDER_SERVICE:
case ActivityManager.INTENT_SENDER_FOREGROUND_SERVICE:
- return new ParceledListSlice<>(mContext.getPackageManager()
- .queryIntentServicesAsUser(intent, matchFlags, userId));
+ return new ParceledListSlice<>(mPackageManagerInt.queryIntentServices(
+ intent, matchFlags, uid, userId));
case ActivityManager.INTENT_SENDER_BROADCAST:
- return new ParceledListSlice<>(mContext.getPackageManager()
- .queryBroadcastReceiversAsUser(intent, matchFlags, userId));
+ return new ParceledListSlice<>(mPackageManagerInt.queryIntentReceivers(
+ intent, resolvedType, matchFlags, uid, userId, false));
default: // ActivityManager.INTENT_SENDER_ACTIVITY_RESULT
throw new IllegalStateException("Unsupported intent sender type: " + res.key.type);
}
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index aedbe4e..b7e817e 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -729,8 +729,11 @@
}
private boolean isDeviceCompatibleWithSpatializationModes(@NonNull AudioDeviceAttributes ada) {
+ // modeForDevice will be neither transaural or binaural for devices that do not support
+ // spatial audio. For instance mono devices like earpiece, speaker safe or sco must
+ // not be included.
final byte modeForDevice = (byte) SPAT_MODE_FOR_DEVICE_TYPE.get(ada.getType(),
- /*default when type not found*/ SpatializationMode.SPATIALIZER_BINAURAL);
+ /*default when type not found*/ -1);
if ((modeForDevice == SpatializationMode.SPATIALIZER_BINAURAL && mBinauralSupported)
|| (modeForDevice == SpatializationMode.SPATIALIZER_TRANSAURAL
&& mTransauralSupported)) {
@@ -1538,8 +1541,8 @@
@Override
public String toString() {
- return "type:" + mDeviceType + " addr:" + mDeviceAddress + " enabled:" + mEnabled
- + " HT:" + mHasHeadTracker + " HTenabled:" + mHeadTrackerEnabled;
+ return "type: " + mDeviceType + " addr: " + mDeviceAddress + " enabled: " + mEnabled
+ + " HT: " + mHasHeadTracker + " HTenabled: " + mHeadTrackerEnabled;
}
String toPersistableString() {
diff --git a/services/core/java/com/android/server/notification/ValidateNotificationPeople.java b/services/core/java/com/android/server/notification/ValidateNotificationPeople.java
index bdc5711..5e0a180 100644
--- a/services/core/java/com/android/server/notification/ValidateNotificationPeople.java
+++ b/services/core/java/com/android/server/notification/ValidateNotificationPeople.java
@@ -131,6 +131,17 @@
}
}
+ // For tests: just do the setting of various local variables without actually doing work
+ @VisibleForTesting
+ protected void initForTests(Context context, NotificationUsageStats usageStats,
+ LruCache peopleCache) {
+ mUserToContextMap = new ArrayMap<>();
+ mBaseContext = context;
+ mUsageStats = usageStats;
+ mPeopleCache = peopleCache;
+ mEnabled = true;
+ }
+
public RankingReconsideration process(NotificationRecord record) {
if (!mEnabled) {
if (VERBOSE) Slog.i(TAG, "disabled");
@@ -179,7 +190,7 @@
return NONE;
}
final PeopleRankingReconsideration prr =
- validatePeople(context, key, extras, null, affinityOut);
+ validatePeople(context, key, extras, null, affinityOut, null);
float affinity = affinityOut[0];
if (prr != null) {
@@ -224,15 +235,21 @@
return context;
}
- private RankingReconsideration validatePeople(Context context,
+ @VisibleForTesting
+ protected RankingReconsideration validatePeople(Context context,
final NotificationRecord record) {
final String key = record.getKey();
final Bundle extras = record.getNotification().extras;
final float[] affinityOut = new float[1];
+ ArraySet<String> phoneNumbersOut = new ArraySet<>();
final PeopleRankingReconsideration rr =
- validatePeople(context, key, extras, record.getPeopleOverride(), affinityOut);
+ validatePeople(context, key, extras, record.getPeopleOverride(), affinityOut,
+ phoneNumbersOut);
final float affinity = affinityOut[0];
record.setContactAffinity(affinity);
+ if (phoneNumbersOut.size() > 0) {
+ record.mergePhoneNumbers(phoneNumbersOut);
+ }
if (rr == null) {
mUsageStats.registerPeopleAffinity(record, affinity > NONE, affinity == STARRED_CONTACT,
true /* cached */);
@@ -243,7 +260,7 @@
}
private PeopleRankingReconsideration validatePeople(Context context, String key, Bundle extras,
- List<String> peopleOverride, float[] affinityOut) {
+ List<String> peopleOverride, float[] affinityOut, ArraySet<String> phoneNumbersOut) {
float affinity = NONE;
if (extras == null) {
return null;
@@ -270,6 +287,15 @@
}
if (lookupResult != null) {
affinity = Math.max(affinity, lookupResult.getAffinity());
+
+ // add all phone numbers associated with this lookup result, if they exist
+ // and if requested
+ if (phoneNumbersOut != null) {
+ ArraySet<String> phoneNumbers = lookupResult.getPhoneNumbers();
+ if (phoneNumbers != null && phoneNumbers.size() > 0) {
+ phoneNumbersOut.addAll(phoneNumbers);
+ }
+ }
}
}
if (++personIdx == MAX_PEOPLE) {
@@ -289,7 +315,8 @@
return new PeopleRankingReconsideration(context, key, pendingLookups);
}
- private String getCacheKey(int userId, String handle) {
+ @VisibleForTesting
+ protected static String getCacheKey(int userId, String handle) {
return Integer.toString(userId) + ":" + handle;
}
@@ -485,7 +512,8 @@
}
}
- private static class LookupResult {
+ @VisibleForTesting
+ protected static class LookupResult {
private static final long CONTACT_REFRESH_MILLIS = 60 * 60 * 1000; // 1hr
private final long mExpireMillis;
@@ -574,7 +602,8 @@
return mPhoneNumbers;
}
- private boolean isExpired() {
+ @VisibleForTesting
+ protected boolean isExpired() {
return mExpireMillis < System.currentTimeMillis();
}
diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index 51b36dd..7159673 100644
--- a/services/core/java/com/android/server/om/OverlayManagerService.java
+++ b/services/core/java/com/android/server/om/OverlayManagerService.java
@@ -39,7 +39,6 @@
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
-import android.app.ActivityManagerInternal;
import android.app.IActivityManager;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -1417,18 +1416,16 @@
private static void broadcastActionOverlayChanged(@NonNull final Set<String> targetPackages,
final int userId) {
- final PackageManagerInternal pmInternal =
- LocalServices.getService(PackageManagerInternal.class);
- final ActivityManagerInternal amInternal =
- LocalServices.getService(ActivityManagerInternal.class);
CollectionUtils.forEach(targetPackages, target -> {
final Intent intent = new Intent(ACTION_OVERLAY_CHANGED,
Uri.fromParts("package", target, null));
intent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
- final int[] allowList = pmInternal.getVisibilityAllowList(target, userId);
- amInternal.broadcastIntent(intent, null /* resultTo */, null /* requiredPermissions */,
- false /* serialized */, userId, allowList, null /* filterExtrasForReceiver */,
- null /* bOptions */);
+ try {
+ ActivityManager.getService().broadcastIntent(null, intent, null, null, 0, null,
+ null, null, android.app.AppOpsManager.OP_NONE, null, false, false, userId);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "broadcastActionOverlayChanged remote exception", e);
+ }
});
}
diff --git a/services/core/java/com/android/server/pm/Computer.java b/services/core/java/com/android/server/pm/Computer.java
index b3c7cc0..ab99860 100644
--- a/services/core/java/com/android/server/pm/Computer.java
+++ b/services/core/java/com/android/server/pm/Computer.java
@@ -113,6 +113,8 @@
@PackageManagerInternal.PrivateResolveFlags long privateResolveFlags,
int filterCallingUid, int userId, boolean resolveForStart, boolean allowDynamicSplits);
@NonNull List<ResolveInfo> queryIntentActivitiesInternal(Intent intent, String resolvedType,
+ long flags, int filterCallingUid, int userId);
+ @NonNull List<ResolveInfo> queryIntentActivitiesInternal(Intent intent, String resolvedType,
long flags, int userId);
@NonNull List<ResolveInfo> queryIntentServicesInternal(Intent intent, String resolvedType,
long flags, int userId, int callingUid, boolean includeInstantApps);
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 7dc648c..7c17778 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -608,6 +608,15 @@
resolveForStart, userId, intent);
}
+ @NonNull
+ @Override
+ public final List<ResolveInfo> queryIntentActivitiesInternal(Intent intent, String resolvedType,
+ @PackageManager.ResolveInfoFlagsBits long flags, int filterCallingUid, int userId) {
+ return queryIntentActivitiesInternal(
+ intent, resolvedType, flags, 0 /*privateResolveFlags*/, filterCallingUid,
+ userId, false /*resolveForStart*/, true /*allowDynamicSplits*/);
+ }
+
public final @NonNull List<ResolveInfo> queryIntentActivitiesInternal(Intent intent,
String resolvedType, @PackageManager.ResolveInfoFlagsBits long flags, int userId) {
return queryIntentActivitiesInternal(
diff --git a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
index c8db297..e969d93 100644
--- a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
+++ b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
@@ -309,7 +309,8 @@
public final List<ResolveInfo> queryIntentActivities(
Intent intent, String resolvedType, @PackageManager.ResolveInfoFlagsBits long flags,
int filterCallingUid, int userId) {
- return snapshot().queryIntentActivitiesInternal(intent, resolvedType, flags, userId);
+ return snapshot().queryIntentActivitiesInternal(intent, resolvedType, flags,
+ filterCallingUid, userId);
}
@Override
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 7dcfd8b..937a789 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -3258,8 +3258,7 @@
@Override
public void onKeyguardOccludedChangedLw(boolean occluded) {
- if (mKeyguardDelegate != null && mKeyguardDelegate.isShowing()
- && !WindowManagerService.sEnableShellTransitions) {
+ if (mKeyguardDelegate != null && mKeyguardDelegate.isShowing()) {
mPendingKeyguardOccluded = occluded;
mKeyguardOccludedChanged = true;
} else {
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index c04b195..4153ea5 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -1655,7 +1655,8 @@
}
// If the window is completely covered by other windows - ignore.
- if (unaccountedSpace.quickReject(regionInScreen)) {
+ Region intersectionWindow = mTempRegion1;
+ if (!intersectionWindow.op(unaccountedSpace, regionInScreen, Region.Op.INTERSECT)) {
return false;
}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 7e94743..765e7f0 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -3245,7 +3245,7 @@
rootTask.moveToFront(reason, task);
// Report top activity change to tracking services and WM
if (mRootWindowContainer.getTopResumedActivity() == this) {
- mAtmService.setResumedActivityUncheckLocked(this, reason);
+ mAtmService.setLastResumedActivityUncheckLocked(this, reason);
}
return true;
}
@@ -5887,7 +5887,8 @@
try {
mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), token,
PauseActivityItem.obtain(finishing, false /* userLeaving */,
- configChangeFlags, false /* dontReport */));
+ configChangeFlags, false /* dontReport */,
+ false /* autoEnteringPip */));
} catch (Exception e) {
Slog.w(TAG, "Exception thrown sending pause: " + intent.getComponent(), e);
}
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index de3b2a6..4003eeb 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -3563,7 +3563,7 @@
// Continue the pausing process after entering pip.
if (r.isState(PAUSING)) {
r.getTask().schedulePauseActivity(r, false /* userLeaving */,
- false /* pauseImmediately */, "auto-pip");
+ false /* pauseImmediately */, true /* autoEnteringPip */, "auto-pip");
}
}
};
@@ -4624,7 +4624,7 @@
}
/** Update AMS states when an activity is resumed. */
- void setResumedActivityUncheckLocked(ActivityRecord r, String reason) {
+ void setLastResumedActivityUncheckLocked(ActivityRecord r, String reason) {
final Task task = r.getTask();
if (task.isActivityTypeStandard()) {
if (mCurAppTimeTracker != r.appTimeTracker) {
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 20032d6..dc91c15 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -2083,7 +2083,7 @@
* activity releases the top state and reports back, message about acquiring top state will be
* sent to the new top resumed activity.
*/
- void updateTopResumedActivityIfNeeded() {
+ void updateTopResumedActivityIfNeeded(String reason) {
final ActivityRecord prevTopActivity = mTopResumedActivity;
final Task topRootTask = mRootWindowContainer.getTopDisplayFocusedRootTask();
if (topRootTask == null || topRootTask.getTopResumedActivity() == prevTopActivity) {
@@ -2119,6 +2119,12 @@
}
mService.updateOomAdj();
}
+ // Update the last resumed activity and focused app when the top resumed activity changed
+ // because the new top resumed activity might be already resumed and thus won't have
+ // activity state change to update the records to AMS.
+ if (mTopResumedActivity != null) {
+ mService.setLastResumedActivityUncheckLocked(mTopResumedActivity, reason);
+ }
scheduleTopResumedActivityStateIfNeeded();
mService.updateTopApp(mTopResumedActivity);
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 552c6a5..077f8b5 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -512,7 +512,7 @@
void onChildPositionChanged(WindowContainer child) {
mWmService.updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL,
!mWmService.mPerDisplayFocusEnabled /* updateInputWindows */);
- mTaskSupervisor.updateTopResumedActivityIfNeeded();
+ mTaskSupervisor.updateTopResumedActivityIfNeeded("onChildPositionChanged");
}
@Override
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 0332935..384a4d1 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -920,7 +920,7 @@
// If the original state is resumed, there is no state change to update focused app.
// So here makes sure the activity focus is set if it is the top.
if (r.isState(RESUMED) && r == mRootWindowContainer.getTopResumedActivity()) {
- mAtmService.setResumedActivityUncheckLocked(r, reason);
+ mAtmService.setLastResumedActivityUncheckLocked(r, reason);
}
}
if (!animate) {
@@ -2433,11 +2433,7 @@
focusableTask.moveToFront(myReason);
// Top display focused root task is changed, update top resumed activity if needed.
if (rootTask.getTopResumedActivity() != null) {
- mTaskSupervisor.updateTopResumedActivityIfNeeded();
- // Set focused app directly because if the next focused activity is already resumed
- // (e.g. the next top activity is on a different display), there won't have activity
- // state change to update it.
- mAtmService.setResumedActivityUncheckLocked(rootTask.getTopResumedActivity(), reason);
+ mTaskSupervisor.updateTopResumedActivityIfNeeded(reason);
}
return rootTask;
}
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index 52bf220..4063cae4 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -323,6 +323,10 @@
// Clear preferred top because the adding focusable task has a higher z-order.
mPreferredTopFocusableRootTask = null;
}
+
+ // Update the top resumed activity because the preferred top focusable task may be changed.
+ mAtmService.mTaskSupervisor.updateTopResumedActivityIfNeeded("addChildTask");
+
mAtmService.updateSleepIfNeededLocked();
onRootTaskOrderChanged(task);
}
@@ -416,12 +420,7 @@
}
// Update the top resumed activity because the preferred top focusable task may be changed.
- mAtmService.mTaskSupervisor.updateTopResumedActivityIfNeeded();
-
- final ActivityRecord r = child.getTopResumedActivity();
- if (r != null && r == mRootWindowContainer.getTopResumedActivity()) {
- mAtmService.setResumedActivityUncheckLocked(r, "positionChildAt");
- }
+ mAtmService.mTaskSupervisor.updateTopResumedActivityIfNeeded("positionChildTaskAt");
if (mChildren.indexOf(child) != oldPosition) {
onRootTaskOrderChanged(child);
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 44b5b88..1b7c6d2 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -460,7 +460,7 @@
final ActivityRecord prevR = mResumedActivity;
mResumedActivity = r;
- mTaskSupervisor.updateTopResumedActivityIfNeeded();
+ mTaskSupervisor.updateTopResumedActivityIfNeeded(reason);
if (r == null && prevR.mDisplayContent != null
&& prevR.mDisplayContent.getFocusedRootTask() == null) {
// Only need to notify DWPC when no activity will resume.
@@ -773,9 +773,6 @@
Slog.v(TAG, "set resumed activity to:" + record + " reason:" + reason);
}
setResumedActivity(record, reason + " - onActivityStateChanged");
- if (record == mRootWindowContainer.getTopResumedActivity()) {
- mAtmService.setResumedActivityUncheckLocked(record, reason);
- }
mTaskSupervisor.mRecentTasks.add(record.getTask());
}
}
@@ -1621,7 +1618,8 @@
ProtoLog.d(WM_DEBUG_STATES, "Auto-PIP allowed, entering PIP mode "
+ "directly: %s, didAutoPip: %b", prev, didAutoPip);
} else {
- schedulePauseActivity(prev, userLeaving, pauseImmediately, reason);
+ schedulePauseActivity(prev, userLeaving, pauseImmediately,
+ false /* autoEnteringPip */, reason);
}
} else {
mPausingActivity = null;
@@ -1675,7 +1673,7 @@
}
void schedulePauseActivity(ActivityRecord prev, boolean userLeaving,
- boolean pauseImmediately, String reason) {
+ boolean pauseImmediately, boolean autoEnteringPip, String reason) {
ProtoLog.v(WM_DEBUG_STATES, "Enqueueing pending pause: %s", prev);
try {
EventLogTags.writeWmPauseActivity(prev.mUserId, System.identityHashCode(prev),
@@ -1683,7 +1681,7 @@
mAtmService.getLifecycleManager().scheduleTransaction(prev.app.getThread(),
prev.token, PauseActivityItem.obtain(prev.finishing, userLeaving,
- prev.configChangeFlags, pauseImmediately));
+ prev.configChangeFlags, pauseImmediately, autoEnteringPip));
} catch (Exception e) {
// Ignore exception, if process died other code will cleanup.
Slog.w(TAG, "Exception thrown during pause", e);
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
index 88059e1..d615583 100644
--- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -49,7 +49,9 @@
import android.window.ITaskFragmentOrganizerController;
import android.window.TaskFragmentInfo;
import android.window.TaskFragmentTransaction;
+import android.window.WindowContainerTransaction;
+import com.android.internal.protolog.ProtoLogGroup;
import com.android.internal.protolog.common.ProtoLog;
import java.lang.annotation.Retention;
@@ -68,6 +70,8 @@
private final ActivityTaskManagerService mAtmService;
private final WindowManagerGlobalLock mGlobalLock;
+ private final WindowOrganizerController mWindowOrganizerController;
+
/**
* A Map which manages the relationship between
* {@link ITaskFragmentOrganizer} and {@link TaskFragmentOrganizerState}
@@ -82,9 +86,11 @@
private final ArraySet<Task> mTmpTaskSet = new ArraySet<>();
- TaskFragmentOrganizerController(ActivityTaskManagerService atm) {
- mAtmService = atm;
+ TaskFragmentOrganizerController(@NonNull ActivityTaskManagerService atm,
+ @NonNull WindowOrganizerController windowOrganizerController) {
+ mAtmService = requireNonNull(atm);
mGlobalLock = atm.mGlobalLock;
+ mWindowOrganizerController = requireNonNull(windowOrganizerController);
}
/**
@@ -131,6 +137,14 @@
private final SparseArray<RemoteAnimationDefinition> mRemoteAnimationDefinitions =
new SparseArray<>();
+ /**
+ * List of {@link TaskFragmentTransaction#getTransactionToken()} that have been sent to the
+ * organizer. If the transaction is sent during a transition, the
+ * {@link TransitionController} will wait until the transaction is finished.
+ * @see #onTransactionFinished(IBinder)
+ */
+ private final List<IBinder> mRunningTransactions = new ArrayList<>();
+
TaskFragmentOrganizerState(ITaskFragmentOrganizer organizer, int pid, int uid) {
mOrganizer = organizer;
mOrganizerPid = pid;
@@ -176,6 +190,10 @@
taskFragment.removeImmediately();
mOrganizedTaskFragments.remove(taskFragment);
}
+ for (int i = mRunningTransactions.size() - 1; i >= 0; i--) {
+ // Cleanup any running transaction to unblock the current transition.
+ onTransactionFinished(mRunningTransactions.get(i));
+ }
mOrganizer.asBinder().unlinkToDeath(this, 0 /*flags*/);
}
@@ -320,6 +338,40 @@
.setActivityIntent(activity.intent)
.setActivityToken(activityToken);
}
+
+ void dispatchTransaction(@NonNull TaskFragmentTransaction transaction) {
+ if (transaction.isEmpty()) {
+ return;
+ }
+ try {
+ mOrganizer.onTransactionReady(transaction);
+ } catch (RemoteException e) {
+ Slog.d(TAG, "Exception sending TaskFragmentTransaction", e);
+ return;
+ }
+ onTransactionStarted(transaction.getTransactionToken());
+ }
+
+ /** Called when the transaction is sent to the organizer. */
+ void onTransactionStarted(@NonNull IBinder transactionToken) {
+ if (!mWindowOrganizerController.getTransitionController().isCollecting()) {
+ return;
+ }
+ ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
+ "Defer transition ready for TaskFragmentTransaction=%s", transactionToken);
+ mRunningTransactions.add(transactionToken);
+ mWindowOrganizerController.getTransitionController().deferTransitionReady();
+ }
+
+ /** Called when the transaction is finished. */
+ void onTransactionFinished(@NonNull IBinder transactionToken) {
+ if (!mRunningTransactions.remove(transactionToken)) {
+ return;
+ }
+ ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
+ "Continue transition ready for TaskFragmentTransaction=%s", transactionToken);
+ mWindowOrganizerController.getTransitionController().continueTransitionReady();
+ }
}
@Nullable
@@ -336,7 +388,7 @@
}
@Override
- public void registerOrganizer(ITaskFragmentOrganizer organizer) {
+ public void registerOrganizer(@NonNull ITaskFragmentOrganizer organizer) {
final int pid = Binder.getCallingPid();
final int uid = Binder.getCallingUid();
synchronized (mGlobalLock) {
@@ -354,7 +406,7 @@
}
@Override
- public void unregisterOrganizer(ITaskFragmentOrganizer organizer) {
+ public void unregisterOrganizer(@NonNull ITaskFragmentOrganizer organizer) {
validateAndGetState(organizer);
final int pid = Binder.getCallingPid();
final long uid = Binder.getCallingUid();
@@ -372,8 +424,8 @@
}
@Override
- public void registerRemoteAnimations(ITaskFragmentOrganizer organizer, int taskId,
- RemoteAnimationDefinition definition) {
+ public void registerRemoteAnimations(@NonNull ITaskFragmentOrganizer organizer, int taskId,
+ @NonNull RemoteAnimationDefinition definition) {
final int pid = Binder.getCallingPid();
final int uid = Binder.getCallingUid();
synchronized (mGlobalLock) {
@@ -398,7 +450,7 @@
}
@Override
- public void unregisterRemoteAnimations(ITaskFragmentOrganizer organizer, int taskId) {
+ public void unregisterRemoteAnimations(@NonNull ITaskFragmentOrganizer organizer, int taskId) {
final int pid = Binder.getCallingPid();
final long uid = Binder.getCallingUid();
synchronized (mGlobalLock) {
@@ -416,6 +468,17 @@
}
}
+ @Override
+ public void onTransactionHandled(@NonNull ITaskFragmentOrganizer organizer,
+ @NonNull IBinder transactionToken, @NonNull WindowContainerTransaction wct) {
+ synchronized (mGlobalLock) {
+ // Keep the calling identity to avoid unsecure change.
+ mWindowOrganizerController.applyTransaction(wct);
+ final TaskFragmentOrganizerState state = validateAndGetState(organizer);
+ state.onTransactionFinished(transactionToken);
+ }
+ }
+
/**
* Gets the {@link RemoteAnimationDefinition} set on the given organizer if exists. Returns
* {@code null} if it doesn't, or if the organizer has activity(ies) embedded in untrusted mode.
@@ -775,13 +838,13 @@
}
final int organizerNum = mPendingTaskFragmentEvents.size();
for (int i = 0; i < organizerNum; i++) {
- final ITaskFragmentOrganizer organizer = mTaskFragmentOrganizerState.get(
- mPendingTaskFragmentEvents.keyAt(i)).mOrganizer;
- dispatchPendingEvents(organizer, mPendingTaskFragmentEvents.valueAt(i));
+ final TaskFragmentOrganizerState state =
+ mTaskFragmentOrganizerState.get(mPendingTaskFragmentEvents.keyAt(i));
+ dispatchPendingEvents(state, mPendingTaskFragmentEvents.valueAt(i));
}
}
- void dispatchPendingEvents(@NonNull ITaskFragmentOrganizer organizer,
+ void dispatchPendingEvents(@NonNull TaskFragmentOrganizerState state,
@NonNull List<PendingTaskFragmentEvent> pendingEvents) {
if (pendingEvents.isEmpty()) {
return;
@@ -817,7 +880,7 @@
if (mTmpTaskSet.add(task)) {
// Make sure the organizer know about the Task config.
transaction.addChange(prepareChange(new PendingTaskFragmentEvent.Builder(
- PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED, organizer)
+ PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED, state.mOrganizer)
.setTask(task)
.build()));
}
@@ -825,7 +888,7 @@
transaction.addChange(prepareChange(event));
}
mTmpTaskSet.clear();
- dispatchTransactionInfo(organizer, transaction);
+ state.dispatchTransaction(transaction);
pendingEvents.removeAll(candidateEvents);
}
@@ -855,6 +918,7 @@
}
final ITaskFragmentOrganizer organizer = taskFragment.getTaskFragmentOrganizer();
+ final TaskFragmentOrganizerState state = validateAndGetState(organizer);
final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
// Make sure the organizer know about the Task config.
transaction.addChange(prepareChange(new PendingTaskFragmentEvent.Builder(
@@ -862,22 +926,10 @@
.setTask(taskFragment.getTask())
.build()));
transaction.addChange(prepareChange(event));
- dispatchTransactionInfo(event.mTaskFragmentOrg, transaction);
+ state.dispatchTransaction(transaction);
mPendingTaskFragmentEvents.get(organizer.asBinder()).remove(event);
}
- private void dispatchTransactionInfo(@NonNull ITaskFragmentOrganizer organizer,
- @NonNull TaskFragmentTransaction transaction) {
- if (transaction.isEmpty()) {
- return;
- }
- try {
- organizer.onTransactionReady(transaction);
- } catch (RemoteException e) {
- Slog.d(TAG, "Exception sending TaskFragmentTransaction", e);
- }
- }
-
@Nullable
private TaskFragmentTransaction.Change prepareChange(
@NonNull PendingTaskFragmentEvent event) {
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 803890b..2d3e437 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1815,6 +1815,8 @@
/** This undoes one call to {@link #deferTransitionReady}. */
void continueTransitionReady() {
--mReadyTracker.mDeferReadyDepth;
+ // Apply ready in case it is waiting for the previous defer call.
+ applyReady();
}
/**
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 4f03264..68b1d35 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -147,7 +147,7 @@
mGlobalLock = atm.mGlobalLock;
mTaskOrganizerController = new TaskOrganizerController(mService);
mDisplayAreaOrganizerController = new DisplayAreaOrganizerController(mService);
- mTaskFragmentOrganizerController = new TaskFragmentOrganizerController(atm);
+ mTaskFragmentOrganizerController = new TaskFragmentOrganizerController(atm, this);
}
void setWindowManager(WindowManagerService wms) {
diff --git a/services/core/jni/OWNERS b/services/core/jni/OWNERS
index 8567110..9abf107 100644
--- a/services/core/jni/OWNERS
+++ b/services/core/jni/OWNERS
@@ -11,7 +11,7 @@
# BatteryStats
per-file com_android_server_am_BatteryStatsService.cpp = file:/BATTERY_STATS_OWNERS
-per-file Android.bp = file:platform/build/soong:/OWNERS
+per-file Android.bp = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION}
per-file com_android_server_Usb* = file:/services/usb/OWNERS
per-file com_android_server_Vibrator* = file:/services/core/java/com/android/server/vibrator/OWNERS
per-file com_android_server_hdmi_* = file:/core/java/android/hardware/hdmi/OWNERS
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 0018a52..4f67f1d 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -5786,29 +5786,8 @@
@VisibleForTesting
public void enforceCallerCanRequestDeviceIdAttestation(CallerIdentity caller)
throws SecurityException {
- /**
- * First check if there's a profile owner because the device could be in COMP mode (where
- * there's a device owner and profile owner on the same device).
- * If the caller is from the work profile, then it must be the PO or the delegate, and
- * it must have the right permission to access device identifiers.
- */
- int callerUserId = caller.getUserId();
- if (hasProfileOwner(callerUserId)) {
- // Make sure that the caller is the profile owner or delegate.
- Preconditions.checkCallAuthorization(canInstallCertificates(caller));
- // Verify that the managed profile is on an organization-owned device (or is affiliated
- // with the device owner user) and as such the profile owner can access Device IDs.
- if (isProfileOwnerOfOrganizationOwnedDevice(callerUserId)
- || isUserAffiliatedWithDevice(callerUserId)) {
- return;
- }
- throw new SecurityException(
- "Profile Owner is not allowed to access Device IDs.");
- }
-
- // If not, fall back to the device owner check.
- Preconditions.checkCallAuthorization(
- isDefaultDeviceOwner(caller) || isCallerDelegate(caller, DELEGATION_CERT_INSTALL));
+ Preconditions.checkCallAuthorization(hasDeviceIdAccessUnchecked(caller.getPackageName(),
+ caller.getUid()));
}
@VisibleForTesting
@@ -5856,7 +5835,6 @@
final boolean deviceIdAttestationRequired = attestationUtilsFlags != null;
KeyGenParameterSpec keySpec = parcelableKeySpec.getSpec();
final String alias = keySpec.getKeystoreAlias();
-
Preconditions.checkStringNotEmpty(alias, "Empty alias provided");
Preconditions.checkArgument(
!deviceIdAttestationRequired || keySpec.getAttestationChallenge() != null,
@@ -9393,26 +9371,33 @@
if (!hasPermission(permission.READ_PHONE_STATE, pid, uid)) {
return false;
}
+ return hasDeviceIdAccessUnchecked(packageName, uid);
+ }
- // Allow access to the device owner or delegate cert installer or profile owner of an
- // affiliated user
+ /**
+ * Check if caller is device owner, delegate cert installer or profile owner of
+ * affiliated user. Or if caller is profile owner for a specified user or delegate cert
+ * installer on an organization-owned device.
+ */
+ private boolean hasDeviceIdAccessUnchecked(String packageName, int uid) {
+ // Is the caller a device owner, delegate cert installer or profile owner of an
+ // affiliated user.
ComponentName deviceOwner = getDeviceOwnerComponent(true);
if (deviceOwner != null && (deviceOwner.getPackageName().equals(packageName)
|| isCallerDelegate(packageName, uid, DELEGATION_CERT_INSTALL))) {
return true;
}
final int userId = UserHandle.getUserId(uid);
- // Allow access to the profile owner for the specified user, or delegate cert installer
- // But only if this is an organization-owned device.
+ // Is the caller the profile owner for the specified user, or delegate cert installer on an
+ // organization-owned device.
ComponentName profileOwner = getProfileOwnerAsUser(userId);
final boolean isCallerProfileOwnerOrDelegate = profileOwner != null
&& (profileOwner.getPackageName().equals(packageName)
- || isCallerDelegate(packageName, uid, DELEGATION_CERT_INSTALL));
+ || isCallerDelegate(packageName, uid, DELEGATION_CERT_INSTALL));
if (isCallerProfileOwnerOrDelegate && (isProfileOwnerOfOrganizationOwnedDevice(userId)
|| isUserAffiliatedWithDevice(userId))) {
return true;
}
-
return false;
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java
index c12f0a9..d72cfc7 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java
@@ -17,7 +17,9 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.ArgumentMatchers.eq;
@@ -30,6 +32,7 @@
import static org.mockito.Mockito.when;
import android.app.Notification;
+import android.app.NotificationChannel;
import android.app.Person;
import android.content.ContentProvider;
import android.content.ContentResolver;
@@ -39,8 +42,11 @@
import android.os.Bundle;
import android.os.UserManager;
import android.provider.ContactsContract;
+import android.service.notification.StatusBarNotification;
import android.test.suitebuilder.annotation.SmallTest;
import android.text.SpannableString;
+import android.util.ArraySet;
+import android.util.LruCache;
import androidx.test.runner.AndroidJUnit4;
@@ -323,6 +329,69 @@
isNull()); // sort order
}
+ @Test
+ public void testValidatePeople_needsLookupWhenNoCache() {
+ final Context mockContext = mock(Context.class);
+ final ContentResolver mockContentResolver = mock(ContentResolver.class);
+ when(mockContext.getContentResolver()).thenReturn(mockContentResolver);
+ final NotificationUsageStats mockNotificationUsageStats =
+ mock(NotificationUsageStats.class);
+
+ // Create validator with empty cache
+ ValidateNotificationPeople vnp = new ValidateNotificationPeople();
+ LruCache cache = new LruCache<String, ValidateNotificationPeople.LookupResult>(5);
+ vnp.initForTests(mockContext, mockNotificationUsageStats, cache);
+
+ NotificationRecord record = getNotificationRecord();
+ String[] callNumber = new String[]{"tel:12345678910"};
+ setNotificationPeople(record, callNumber);
+
+ // Returned ranking reconsideration not null indicates that there is a lookup to be done
+ RankingReconsideration rr = vnp.validatePeople(mockContext, record);
+ assertNotNull(rr);
+ }
+
+ @Test
+ public void testValidatePeople_noLookupWhenCached_andPopulatesContactInfo() {
+ final Context mockContext = mock(Context.class);
+ final ContentResolver mockContentResolver = mock(ContentResolver.class);
+ when(mockContext.getContentResolver()).thenReturn(mockContentResolver);
+ when(mockContext.getUserId()).thenReturn(1);
+ final NotificationUsageStats mockNotificationUsageStats =
+ mock(NotificationUsageStats.class);
+
+ // Information to be passed in & returned from the lookup result
+ String lookup = "lookup:contactinfohere";
+ String lookupTel = "16175551234";
+ float affinity = 0.7f;
+
+ // Create a fake LookupResult for the data we'll pass in
+ LruCache cache = new LruCache<String, ValidateNotificationPeople.LookupResult>(5);
+ ValidateNotificationPeople.LookupResult lr =
+ mock(ValidateNotificationPeople.LookupResult.class);
+ when(lr.getAffinity()).thenReturn(affinity);
+ when(lr.getPhoneNumbers()).thenReturn(new ArraySet<>(new String[]{lookupTel}));
+ when(lr.isExpired()).thenReturn(false);
+ cache.put(ValidateNotificationPeople.getCacheKey(1, lookup), lr);
+
+ // Create validator with the established cache
+ ValidateNotificationPeople vnp = new ValidateNotificationPeople();
+ vnp.initForTests(mockContext, mockNotificationUsageStats, cache);
+
+ NotificationRecord record = getNotificationRecord();
+ String[] peopleInfo = new String[]{lookup};
+ setNotificationPeople(record, peopleInfo);
+
+ // Returned ranking reconsideration null indicates that there is no pending work to be done
+ RankingReconsideration rr = vnp.validatePeople(mockContext, record);
+ assertNull(rr);
+
+ // Confirm that the affinity & phone number made it into our record
+ assertEquals(affinity, record.getContactAffinity(), 1e-8);
+ assertNotNull(record.getPhoneNumbers());
+ assertTrue(record.getPhoneNumbers().contains(lookupTel));
+ }
+
// Creates a cursor that points to one item of Contacts data with the specified
// columns.
private Cursor makeMockCursor(int id, String lookupKey, int starred, int hasPhone) {
@@ -365,4 +434,17 @@
String resultString = Arrays.toString(result);
assertEquals(message + ": arrays differ", expectedString, resultString);
}
+
+ private NotificationRecord getNotificationRecord() {
+ StatusBarNotification sbn = mock(StatusBarNotification.class);
+ Notification notification = mock(Notification.class);
+ when(sbn.getNotification()).thenReturn(notification);
+ return new NotificationRecord(mContext, sbn, mock(NotificationChannel.class));
+ }
+
+ private void setNotificationPeople(NotificationRecord r, String[] people) {
+ Bundle extras = new Bundle();
+ extras.putObject(Notification.EXTRA_PEOPLE_LIST, people);
+ r.getSbn().getNotification().extras = extras;
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
index 75ecfd8..d5e336b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
@@ -228,7 +228,7 @@
mAtm.getTaskChangeNotificationController();
spyOn(taskChangeNotifier);
- mAtm.setResumedActivityUncheckLocked(fullScreenActivityA, "resumeA");
+ mAtm.setLastResumedActivityUncheckLocked(fullScreenActivityA, "resumeA");
verify(taskChangeNotifier).notifyTaskFocusChanged(eq(taskA.mTaskId) /* taskId */,
eq(true) /* focused */);
reset(taskChangeNotifier);
@@ -237,7 +237,7 @@
.build();
final Task taskB = fullScreenActivityB.getTask();
- mAtm.setResumedActivityUncheckLocked(fullScreenActivityB, "resumeB");
+ mAtm.setLastResumedActivityUncheckLocked(fullScreenActivityB, "resumeB");
verify(taskChangeNotifier).notifyTaskFocusChanged(eq(taskA.mTaskId) /* taskId */,
eq(false) /* focused */);
verify(taskChangeNotifier).notifyTaskFocusChanged(eq(taskB.mTaskId) /* taskId */,
@@ -295,6 +295,7 @@
activity1.moveFocusableActivityToTop("test");
assertEquals(activity1.getUid(), pendingTopUid[0]);
verify(mAtm).updateOomAdj();
+ verify(mAtm).setLastResumedActivityUncheckLocked(any(), eq("test"));
}
/**
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index da72030..9274eb3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -25,6 +25,7 @@
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.server.wm.TaskFragment.EMBEDDING_ALLOWED;
@@ -46,7 +47,6 @@
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.times;
import static org.mockito.Mockito.verify;
@@ -65,6 +65,7 @@
import android.window.TaskFragmentInfo;
import android.window.TaskFragmentOrganizer;
import android.window.TaskFragmentOrganizerToken;
+import android.window.TaskFragmentTransaction;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
import android.window.WindowContainerTransactionCallback;
@@ -90,6 +91,7 @@
private TaskFragmentOrganizerController mController;
private WindowOrganizerController mWindowOrganizerController;
+ private TransitionController mTransitionController;
private TaskFragmentOrganizer mOrganizer;
private TaskFragmentOrganizerToken mOrganizerToken;
private ITaskFragmentOrganizer mIOrganizer;
@@ -107,9 +109,10 @@
private Task mTask;
@Before
- public void setup() {
+ public void setup() throws RemoteException {
MockitoAnnotations.initMocks(this);
mWindowOrganizerController = mAtm.mWindowOrganizerController;
+ mTransitionController = mWindowOrganizerController.mTransitionController;
mController = mWindowOrganizerController.mTaskFragmentOrganizerController;
mOrganizer = new TaskFragmentOrganizer(Runnable::run);
mOrganizerToken = mOrganizer.getOrganizerToken();
@@ -128,11 +131,16 @@
spyOn(mController);
spyOn(mOrganizer);
spyOn(mTaskFragment);
+ spyOn(mWindowOrganizerController);
+ spyOn(mTransitionController);
doReturn(mIOrganizer).when(mTaskFragment).getTaskFragmentOrganizer();
doReturn(mTaskFragmentInfo).when(mTaskFragment).getTaskFragmentInfo();
doReturn(new SurfaceControl()).when(mTaskFragment).getSurfaceControl();
doReturn(mFragmentToken).when(mTaskFragment).getFragmentToken();
doReturn(new Configuration()).when(mTaskFragmentInfo).getConfiguration();
+
+ // To prevent it from calling the real server.
+ doNothing().when(mOrganizer).onTransactionHandled(any(), any());
}
@Test
@@ -866,7 +874,7 @@
assertFalse(parentTask.shouldBeVisible(null));
// Verify the info changed callback still occurred despite the task being invisible
- reset(mOrganizer);
+ clearInvocations(mOrganizer);
mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
mController.dispatchPendingEvents();
verify(mOrganizer).onTaskFragmentInfoChanged(any(), any());
@@ -899,7 +907,7 @@
verify(mOrganizer).onTaskFragmentInfoChanged(any(), any());
// Verify the info changed callback is not called when the task is invisible
- reset(mOrganizer);
+ clearInvocations(mOrganizer);
doReturn(false).when(task).shouldBeVisible(any());
mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
mController.dispatchPendingEvents();
@@ -1092,6 +1100,40 @@
.that(mTaskFragment.getBounds()).isEqualTo(task.getBounds());
}
+ @Test
+ public void testOnTransactionReady_invokeOnTransactionHandled() {
+ mController.registerOrganizer(mIOrganizer);
+ final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
+ mOrganizer.onTransactionReady(transaction);
+
+ // Organizer should always trigger #onTransactionHandled when receives #onTransactionReady
+ verify(mOrganizer).onTransactionHandled(eq(transaction.getTransactionToken()), any());
+ verify(mOrganizer, never()).applyTransaction(any());
+ }
+
+ @Test
+ public void testDispatchTransaction_deferTransitionReady() {
+ mController.registerOrganizer(mIOrganizer);
+ setupMockParent(mTaskFragment, mTask);
+ final ArgumentCaptor<IBinder> tokenCaptor = ArgumentCaptor.forClass(IBinder.class);
+ final ArgumentCaptor<WindowContainerTransaction> wctCaptor =
+ ArgumentCaptor.forClass(WindowContainerTransaction.class);
+ doReturn(true).when(mTransitionController).isCollecting();
+
+ mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
+ mController.dispatchPendingEvents();
+
+ // Defer transition when send TaskFragment transaction during transition collection.
+ verify(mTransitionController).deferTransitionReady();
+ verify(mOrganizer).onTransactionHandled(tokenCaptor.capture(), wctCaptor.capture());
+
+ mController.onTransactionHandled(mIOrganizer, tokenCaptor.getValue(), wctCaptor.getValue());
+
+ // Apply the organizer change and continue transition.
+ verify(mWindowOrganizerController).applyTransaction(wctCaptor.getValue());
+ verify(mTransitionController).continueTransitionReady();
+ }
+
/**
* Creates a {@link TaskFragment} with the {@link WindowContainerTransaction}. Calls
* {@link WindowOrganizerController#applyTransaction} to apply the transaction,