summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/app/HomeVisibilityListener.java5
-rw-r--r--core/java/android/app/IProcessObserver.aidl11
-rw-r--r--core/java/android/app/Notification.java28
-rw-r--r--core/java/android/os/PersistableBundle.java37
-rw-r--r--core/java/android/os/UserManager.java23
-rw-r--r--core/java/com/android/internal/app/ConfirmUserCreationActivity.java12
-rw-r--r--core/java/com/android/internal/widget/ConversationLayout.java37
-rw-r--r--core/java/com/android/internal/widget/PeopleHelper.java68
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt8
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java6
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java30
-rw-r--r--media/java/android/media/RoutingSessionInfo.java8
-rw-r--r--packages/SettingsLib/res/drawable/ic_external_display.xml28
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt31
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java17
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java1
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java99
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java22
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java102
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterLogger.kt24
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java74
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt390
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineConversationViewBinder.kt40
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineViewBinder.kt34
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/SingleLineViewModel.kt67
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt17
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java6
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt48
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java6
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineConversationViewBinderTest.kt117
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt91
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt463
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt32
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt4
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerShellCommand.java5
-rw-r--r--services/core/java/com/android/server/am/AppFGSTracker.java5
-rw-r--r--services/core/java/com/android/server/am/ProcessList.java17
-rw-r--r--services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java5
-rw-r--r--services/core/java/com/android/server/devicestate/DeviceStateManagerService.java4
-rw-r--r--services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java5
-rw-r--r--services/core/java/com/android/server/os/BugreportManagerServiceImpl.java11
-rw-r--r--services/core/java/com/android/server/pm/UserManagerService.java29
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java275
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java2
-rw-r--r--services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java32
-rw-r--r--services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java102
60 files changed, 2099 insertions, 473 deletions
diff --git a/core/java/android/app/HomeVisibilityListener.java b/core/java/android/app/HomeVisibilityListener.java
index 5dd7ab0f99fa..1f5f2e4c8237 100644
--- a/core/java/android/app/HomeVisibilityListener.java
+++ b/core/java/android/app/HomeVisibilityListener.java
@@ -69,11 +69,6 @@ public abstract class HomeVisibilityListener {
public HomeVisibilityListener() {
mObserver = new android.app.IProcessObserver.Stub() {
@Override
- public void onProcessStarted(int pid, int processUid, int packageUid,
- String packageName, String processName) {
- }
-
- @Override
public void onForegroundActivitiesChanged(int pid, int uid, boolean fg) {
refreshHomeVisibility();
}
diff --git a/core/java/android/app/IProcessObserver.aidl b/core/java/android/app/IProcessObserver.aidl
index 5c5e72cf9d6f..7be3620f317b 100644
--- a/core/java/android/app/IProcessObserver.aidl
+++ b/core/java/android/app/IProcessObserver.aidl
@@ -18,17 +18,6 @@ package android.app;
/** {@hide} */
oneway interface IProcessObserver {
- /**
- * Invoked when an app process starts up.
- *
- * @param pid The pid of the process.
- * @param processUid The UID associated with the process.
- * @param packageUid The UID associated with the package.
- * @param packageName The name of the package.
- * @param processName The name of the process.
- */
- void onProcessStarted(int pid, int processUid, int packageUid,
- @utf8InCpp String packageName, @utf8InCpp String processName);
void onForegroundActivitiesChanged(int pid, int uid, boolean foregroundActivities);
void onForegroundServicesChanged(int pid, int uid, int serviceTypes);
void onProcessDied(int pid, int uid);
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index ed0cfbe3d9c3..a81ad3c429ea 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -5487,6 +5487,15 @@ public class Notification implements Parcelable
return mColors;
}
+ /**
+ * @param isHeader If the notification is a notification header
+ * @return An instance of mColors after resolving the palette
+ */
+ private Colors getColors(boolean isHeader) {
+ mColors.resolvePalette(mContext, mN.color, !isHeader && mN.isColorized(), mInNightMode);
+ return mColors;
+ }
+
private void updateBackgroundColor(RemoteViews contentView,
StandardTemplateParams p) {
if (isBackgroundColorized(p)) {
@@ -6618,6 +6627,23 @@ public class Notification implements Parcelable
return getColors(p).getContrastColor();
}
+ /**
+ * Gets the foreground color of the small icon. If the notification is colorized, this
+ * is the primary text color, otherwise it's the contrast-adjusted app-provided color.
+ * @hide
+ */
+ public @ColorInt int getSmallIconColor(boolean isHeader) {
+ return getColors(/* isHeader = */ isHeader).getContrastColor();
+ }
+
+ /**
+ * Gets the background color of the notification.
+ * @hide
+ */
+ public @ColorInt int getBackgroundColor(boolean isHeader) {
+ return getColors(/* isHeader = */ isHeader).getBackgroundColor();
+ }
+
/** @return the theme's accent color for colored UI elements. */
private @ColorInt int getPrimaryAccentColor(StandardTemplateParams p) {
return getColors(p).getPrimaryAccentColor();
@@ -8532,6 +8558,8 @@ public class Notification implements Parcelable
boolean isImportantConversation = mConversationType == CONVERSATION_TYPE_IMPORTANT;
boolean isHeaderless = !isConversationLayout && isCollapsed;
+ //TODO (b/217799515): ensure mConversationTitle always returns the correct
+ // conversationTitle, probably set mConversationTitle = conversationTitle after this
CharSequence conversationTitle = !TextUtils.isEmpty(super.mBigContentTitle)
? super.mBigContentTitle
: mConversationTitle;
diff --git a/core/java/android/os/PersistableBundle.java b/core/java/android/os/PersistableBundle.java
index 02704f5b346b..236194d16ad8 100644
--- a/core/java/android/os/PersistableBundle.java
+++ b/core/java/android/os/PersistableBundle.java
@@ -294,6 +294,43 @@ public final class PersistableBundle extends BaseBundle implements Cloneable, Pa
XmlUtils.writeMapXml(mMap, out, this);
}
+ /**
+ * Checks whether all keys and values are within the given character limit.
+ * Note: Maximum character limit of String that can be saved to XML as part of bundle is 65535.
+ * Otherwise IOException is thrown.
+ * @param limit length of String keys and values in the PersistableBundle, including nested
+ * PersistableBundles to check against.
+ *
+ * @hide
+ */
+ public boolean isBundleContentsWithinLengthLimit(int limit) {
+ unparcel();
+ if (mMap == null) {
+ return true;
+ }
+ for (int i = 0; i < mMap.size(); i++) {
+ if (mMap.keyAt(i) != null && mMap.keyAt(i).length() > limit) {
+ return false;
+ }
+ final Object value = mMap.valueAt(i);
+ if (value instanceof String && ((String) value).length() > limit) {
+ return false;
+ } else if (value instanceof String[]) {
+ String[] stringArray = (String[]) value;
+ for (int j = 0; j < stringArray.length; j++) {
+ if (stringArray[j] != null
+ && stringArray[j].length() > limit) {
+ return false;
+ }
+ }
+ } else if (value instanceof PersistableBundle
+ && !((PersistableBundle) value).isBundleContentsWithinLengthLimit(limit)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
/** @hide */
static class MyReadMapCallback implements XmlUtils.ReadMapCallback {
@Override
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 91ef2fa4cf88..533946d89706 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -107,6 +107,21 @@ public class UserManager {
/** Whether the device is in headless system user mode; null until cached. */
private static Boolean sIsHeadlessSystemUser = null;
+ /** Maximum length of username.
+ * @hide
+ */
+ public static final int MAX_USER_NAME_LENGTH = 100;
+
+ /** Maximum length of user property String value.
+ * @hide
+ */
+ public static final int MAX_ACCOUNT_STRING_LENGTH = 500;
+
+ /** Maximum length of account options String values.
+ * @hide
+ */
+ public static final int MAX_ACCOUNT_OPTIONS_LENGTH = 1000;
+
/**
* User type representing a {@link UserHandle#USER_SYSTEM system} user that is a human user.
* This type of user cannot be created; it can only pre-exist on first boot.
@@ -4431,15 +4446,15 @@ public class UserManager {
* This API should only be called if the current user is an {@link #isAdminUser() admin} user,
* as otherwise the returned intent will not be able to create a user.
*
- * @param userName Optional name to assign to the user.
+ * @param userName Optional name to assign to the user. Character limit is 100.
* @param accountName Optional account name that will be used by the setup wizard to initialize
- * the user.
+ * the user. Character limit is 500.
* @param accountType Optional account type for the account to be created. This is required
- * if the account name is specified.
+ * if the account name is specified. Character limit is 500.
* @param accountOptions Optional bundle of data to be passed in during account creation in the
* new user via {@link AccountManager#addAccount(String, String, String[],
* Bundle, android.app.Activity, android.accounts.AccountManagerCallback,
- * Handler)}.
+ * Handler)}. Character limit is 1000.
* @return An Intent that can be launched from an Activity.
* @see #USER_CREATION_FAILED_NOT_PERMITTED
* @see #USER_CREATION_FAILED_NO_MORE_USERS
diff --git a/core/java/com/android/internal/app/ConfirmUserCreationActivity.java b/core/java/com/android/internal/app/ConfirmUserCreationActivity.java
index 0a28997885ff..b4e87498f09b 100644
--- a/core/java/com/android/internal/app/ConfirmUserCreationActivity.java
+++ b/core/java/com/android/internal/app/ConfirmUserCreationActivity.java
@@ -116,6 +116,14 @@ public class ConfirmUserCreationActivity extends AlertActivity
if (cantCreateUser) {
setResult(UserManager.USER_CREATION_FAILED_NOT_PERMITTED);
return null;
+ } else if (!(isUserPropertyWithinLimit(mUserName, UserManager.MAX_USER_NAME_LENGTH)
+ && isUserPropertyWithinLimit(mAccountName, UserManager.MAX_ACCOUNT_STRING_LENGTH)
+ && isUserPropertyWithinLimit(mAccountType, UserManager.MAX_ACCOUNT_STRING_LENGTH))
+ || (mAccountOptions != null && !mAccountOptions.isBundleContentsWithinLengthLimit(
+ UserManager.MAX_ACCOUNT_OPTIONS_LENGTH))) {
+ setResult(UserManager.USER_CREATION_FAILED_NOT_PERMITTED);
+ Log.i(TAG, "User properties must not exceed their character limits");
+ return null;
} else if (cantCreateAnyMoreUsers) {
setResult(UserManager.USER_CREATION_FAILED_NO_MORE_USERS);
return null;
@@ -144,4 +152,8 @@ public class ConfirmUserCreationActivity extends AlertActivity
}
finish();
}
+
+ private boolean isUserPropertyWithinLimit(String property, int limit) {
+ return property == null || property.length() <= limit;
+ }
}
diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java
index 42be784d8baa..a8d0d37f78bd 100644
--- a/core/java/com/android/internal/widget/ConversationLayout.java
+++ b/core/java/com/android/internal/widget/ConversationLayout.java
@@ -105,6 +105,9 @@ public class ConversationLayout extends FrameLayout
private int mConversationIconTopPaddingExpandedGroup;
private int mConversationIconTopPadding;
private int mExpandedGroupMessagePadding;
+ // TODO (b/217799515) Currently, mConversationText shows the conversation title, the actual
+ // conversation text is inside of mMessagingLinearLayout, which is misleading, we should rename
+ // this to mConversationTitleView
private TextView mConversationText;
private View mConversationIconBadge;
private CachingIconView mConversationIconBadgeBg;
@@ -125,6 +128,11 @@ public class ConversationLayout extends FrameLayout
private int mNotificationBackgroundColor;
private CharSequence mFallbackChatName;
private CharSequence mFallbackGroupChatName;
+ //TODO (b/217799515) Currently, Notification.MessagingStyle, ConversationLayout, and
+ // HybridConversationNotificationView, each has their own definition of "ConversationTitle".
+ // What make things worse is that the term of "ConversationTitle" often confuses with
+ // "ConversationText".
+ // We need to unify them or differentiate the namings.
private CharSequence mConversationTitle;
private int mMessageSpacingStandard;
private int mMessageSpacingGroup;
@@ -160,12 +168,12 @@ public class ConversationLayout extends FrameLayout
}
public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs,
- @AttrRes int defStyleAttr) {
+ @AttrRes int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs,
- @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+ @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@@ -297,13 +305,17 @@ public class ConversationLayout extends FrameLayout
mNameReplacement = nameReplacement;
}
- /** Sets this conversation as "important", adding some additional UI treatment. */
+ /**
+ * Sets this conversation as "important", adding some additional UI treatment.
+ */
@RemotableViewMethod
public void setIsImportantConversation(boolean isImportantConversation) {
setIsImportantConversation(isImportantConversation, false);
}
- /** @hide **/
+ /**
+ * @hide
+ **/
public void setIsImportantConversation(boolean isImportantConversation, boolean animate) {
mImportantConversation = isImportantConversation;
mImportanceRingView.setVisibility(isImportantConversation && mIcon.getVisibility() != GONE
@@ -386,6 +398,7 @@ public class ConversationLayout extends FrameLayout
/**
* Set conversation data
+ *
* @param extras Bundle contains conversation data
*/
@RemotableViewMethod(asyncImpl = "setDataAsync")
@@ -427,6 +440,7 @@ public class ConversationLayout extends FrameLayout
* RemotableViewMethod's asyncImpl of {@link #setData(Bundle)}.
* This should be called on a background thread, and returns a Runnable which is then must be
* called on the main thread to complete the operation and set text.
+ *
* @param extras Bundle contains conversation data
* @hide
*/
@@ -449,6 +463,7 @@ public class ConversationLayout extends FrameLayout
/**
* enable/disable precomputed text usage
+ *
* @hide
*/
public void setPrecomputedTextEnabled(boolean precomputedTextEnabled) {
@@ -466,7 +481,9 @@ public class ConversationLayout extends FrameLayout
mImageResolver = resolver;
}
- /** @hide */
+ /**
+ * @hide
+ */
public void setUnreadCount(int unreadCount) {
mExpandButton.setNumber(unreadCount);
}
@@ -795,6 +812,10 @@ public class ConversationLayout extends FrameLayout
mConversationTitle = conversationTitle != null ? conversationTitle.toString() : null;
}
+ // TODO (b/217799515) getConversationTitle is not consistent with setConversationTitle
+ // if you call getConversationTitle() immediately after setConversationTitle(), the result
+ // will not correctly reflect the new change without calling updateConversationLayout, for
+ // example.
public CharSequence getConversationTitle() {
return mConversationText.getText();
}
@@ -914,7 +935,7 @@ public class ConversationLayout extends FrameLayout
}
private void createGroupViews(List<List<MessagingMessage>> groups,
- List<Person> senders, boolean showSpinner) {
+ List<Person> senders, boolean showSpinner) {
mGroups.clear();
for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) {
List<MessagingMessage> group = groups.get(groupIndex);
@@ -963,8 +984,8 @@ public class ConversationLayout extends FrameLayout
}
private void findGroups(List<MessagingMessage> historicMessages,
- List<MessagingMessage> messages, List<List<MessagingMessage>> groups,
- List<Person> senders) {
+ List<MessagingMessage> messages, List<List<MessagingMessage>> groups,
+ List<Person> senders) {
CharSequence currentSenderKey = null;
List<MessagingMessage> currentGroup = null;
int histSize = historicMessages.size();
diff --git a/core/java/com/android/internal/widget/PeopleHelper.java b/core/java/com/android/internal/widget/PeopleHelper.java
index 85cedc362b99..3f5b4a0d61fe 100644
--- a/core/java/com/android/internal/widget/PeopleHelper.java
+++ b/core/java/com/android/internal/widget/PeopleHelper.java
@@ -22,6 +22,8 @@ import static com.android.internal.widget.MessagingPropertyAnimator.ALPHA_OUT;
import android.annotation.ColorInt;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.Notification;
+import android.app.Person;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
@@ -222,6 +224,72 @@ public class PeopleHelper {
}
/**
+ * A class that represents a map from unique sender names in the groups to the string 1- or
+ * 2-character prefix strings for the names. This class uses the String value of the
+ * CharSequence Names as the key.
+ */
+ public class NameToPrefixMap {
+ Map<String, String> mMap;
+ NameToPrefixMap(Map<String, String> map) {
+ this.mMap = map;
+ }
+
+ /**
+ * @param name the name
+ * @return the prefix of the given name
+ */
+ public String getPrefix(CharSequence name) {
+ return mMap.get(name.toString());
+ }
+ }
+
+ /**
+ * Same functionality as mapUniqueNamesToPrefix, but takes list-represented message groups as
+ * the input. This method is better when inflating MessagingGroup from the UI thread is not
+ * an option.
+ * @param groups message groups represented by lists. A message group is some consecutive
+ * messages (>=3) from the same sender in a conversation.
+ */
+ public NameToPrefixMap mapUniqueNamesToPrefixWithGroupList(
+ List<List<Notification.MessagingStyle.Message>> groups) {
+ // Map of unique names to their prefix
+ ArrayMap<String, String> uniqueNames = new ArrayMap<>();
+ // Map of single-character string prefix to the only name which uses it, or null if multiple
+ ArrayMap<String, CharSequence> uniqueCharacters = new ArrayMap<>();
+ for (int i = 0; i < groups.size(); i++) {
+ List<Notification.MessagingStyle.Message> group = groups.get(i);
+ if (group.isEmpty()) continue;
+ Person sender = group.get(0).getSenderPerson();
+ if (sender == null) continue;
+ CharSequence senderName = sender.getName();
+ if (sender.getIcon() != null || TextUtils.isEmpty(senderName)) {
+ continue;
+ }
+ String senderNameString = senderName.toString();
+ if (!uniqueNames.containsKey(senderNameString)) {
+ String charPrefix = findNamePrefix(senderName, null);
+ if (charPrefix == null) {
+ continue;
+ }
+ if (uniqueCharacters.containsKey(charPrefix)) {
+ // this character was already used, lets make it more unique. We first need to
+ // resolve the existing character if it exists
+ CharSequence existingName = uniqueCharacters.get(charPrefix);
+ if (existingName != null) {
+ uniqueNames.put(existingName.toString(), findNameSplit(existingName));
+ uniqueCharacters.put(charPrefix, null);
+ }
+ uniqueNames.put(senderNameString, findNameSplit(senderName));
+ } else {
+ uniqueNames.put(senderNameString, charPrefix);
+ uniqueCharacters.put(charPrefix, senderName);
+ }
+ }
+ }
+ return new NameToPrefixMap(uniqueNames);
+ }
+
+ /**
* Update whether the groups can hide the sender if they are first
* (happens only for 1:1 conversations where the given title matches the sender's name)
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
index 2dd27430e348..dbf7186def8a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
@@ -80,8 +80,7 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract {
Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onRestartButtonClicked) {
super(context, taskInfo, syncQueue, taskListener, displayLayout);
mCallback = callback;
- mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat
- && shouldShowSizeCompatRestartButton(taskInfo);
+ mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat;
mCameraCompatControlState = taskInfo.appCompatTaskInfo.cameraCompatControlState;
mCompatUIHintsState = compatUIHintsState;
mCompatUIConfiguration = compatUIConfiguration;
@@ -106,7 +105,8 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract {
@Override
protected boolean eligibleToShowLayout() {
- return mHasSizeCompat || shouldShowCameraControl();
+ return (mHasSizeCompat && shouldShowSizeCompatRestartButton(getLastTaskInfo()))
+ || shouldShowCameraControl();
}
@Override
@@ -114,11 +114,6 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract {
mLayout = inflateLayout();
mLayout.inject(this);
- final TaskInfo taskInfo = getLastTaskInfo();
- if (taskInfo != null) {
- mHasSizeCompat = mHasSizeCompat && shouldShowSizeCompatRestartButton(taskInfo);
- }
-
updateVisibilityOfViews();
if (mHasSizeCompat) {
@@ -139,8 +134,7 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract {
boolean canShow) {
final boolean prevHasSizeCompat = mHasSizeCompat;
final int prevCameraCompatControlState = mCameraCompatControlState;
- mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat
- && shouldShowSizeCompatRestartButton(taskInfo);
+ mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat;
mCameraCompatControlState = taskInfo.appCompatTaskInfo.cameraCompatControlState;
if (!super.updateCompatInfo(taskInfo, taskListener, canShow)) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
index 180498c50c78..0564c95aef5c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
@@ -332,7 +332,7 @@ public abstract class CompatUIWindowManagerAbstract extends WindowlessWindowMana
updateSurfacePosition();
}
- @Nullable
+ @NonNull
protected TaskInfo getLastTaskInfo() {
return mTaskInfo;
}
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt
index 0d1853534927..47bff8de377e 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt
@@ -78,14 +78,6 @@ abstract class TvPipTestBase : PipTestBase(rotationToString(ROTATION_0), ROTATIO
uiAutomation.dropShellPermissionIdentity()
}
- override fun onProcessStarted(
- pid: Int,
- processUid: Int,
- packageUid: Int,
- packageName: String,
- processName: String
- ) {}
-
override fun onForegroundActivitiesChanged(pid: Int, uid: Int, foreground: Boolean) {}
override fun onForegroundServicesChanged(pid: Int, uid: Int, serviceTypes: Int) {}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
index 4ddc539eb220..dd358e757fde 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
@@ -30,6 +30,7 @@ import static org.mockito.Mockito.verify;
import android.app.ActivityManager;
import android.app.AppCompatTaskInfo.CameraCompatControlState;
import android.app.TaskInfo;
+import android.graphics.Rect;
import android.testing.AndroidTestingRunner;
import android.util.Pair;
import android.view.LayoutInflater;
@@ -83,6 +84,7 @@ public class CompatUILayoutTest extends ShellTestCase {
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
+ doReturn(100).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance();
mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN);
mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue,
mCallback, mTaskListener, new DisplayLayout(), new CompatUIHintsState(),
@@ -127,7 +129,6 @@ public class CompatUILayoutTest extends ShellTestCase {
@Test
public void testOnClickForSizeCompatHint() {
mWindowManager.mHasSizeCompat = true;
- doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(mTaskInfo);
mWindowManager.createLayout(/* canShow= */ true);
final LinearLayout sizeCompatHint = mLayout.findViewById(R.id.size_compat_hint);
sizeCompatHint.performClick();
@@ -222,6 +223,9 @@ public class CompatUILayoutTest extends ShellTestCase {
taskInfo.taskId = TASK_ID;
taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat;
taskInfo.appCompatTaskInfo.cameraCompatControlState = cameraCompatControlState;
+ taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 1000;
+ taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1000;
+ taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 2000, 2000));
return taskInfo;
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
index 2acfd83084ab..4f261cd79d39 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
@@ -20,7 +20,6 @@ import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED;
import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
import static android.view.WindowInsets.Type.navigationBars;
@@ -86,6 +85,8 @@ public class CompatUIWindowManagerTest extends ShellTestCase {
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
private static final int TASK_ID = 1;
+ private static final int TASK_WIDTH = 2000;
+ private static final int TASK_HEIGHT = 2000;
@Mock private SyncTransactionQueue mSyncTransactionQueue;
@Mock private CompatUIController.CompatUICallback mCallback;
@@ -101,6 +102,7 @@ public class CompatUIWindowManagerTest extends ShellTestCase {
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
+ doReturn(100).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance();
mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN);
mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue,
mCallback, mTaskListener, new DisplayLayout(), new CompatUIHintsState(),
@@ -115,7 +117,6 @@ public class CompatUIWindowManagerTest extends ShellTestCase {
public void testCreateSizeCompatButton() {
// Doesn't create layout if show is false.
mWindowManager.mHasSizeCompat = true;
- doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(mTaskInfo);
assertTrue(mWindowManager.createLayout(/* canShow= */ false));
verify(mWindowManager, never()).inflateLayout();
@@ -147,6 +148,13 @@ public class CompatUIWindowManagerTest extends ShellTestCase {
mWindowManager.mHasSizeCompat = false;
assertFalse(mWindowManager.createLayout(/* canShow= */ true));
+ // Returns false and doesn't create layout if restart button should be hidden.
+ clearInvocations(mWindowManager);
+ mWindowManager.mHasSizeCompat = true;
+ mTaskInfo.appCompatTaskInfo.topActivityLetterboxWidth = TASK_WIDTH;
+ mTaskInfo.appCompatTaskInfo.topActivityLetterboxHeight = TASK_HEIGHT;
+ assertFalse(mWindowManager.createLayout(/* canShow= */ true));
+
verify(mWindowManager, never()).inflateLayout();
}
@@ -293,8 +301,6 @@ public class CompatUIWindowManagerTest extends ShellTestCase {
@Test
public void testUpdateCompatInfoLayoutNotInflatedYet() {
- mWindowManager.mHasSizeCompat = true;
- doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(any());
mWindowManager.createLayout(/* canShow= */ false);
verify(mWindowManager, never()).inflateLayout();
@@ -314,6 +320,15 @@ public class CompatUIWindowManagerTest extends ShellTestCase {
mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true);
verify(mWindowManager).inflateLayout();
+
+ // Change shouldShowSizeCompatRestartButton to false and pass canShow true, layout
+ // shouldn't be inflated
+ clearInvocations(mWindowManager);
+ taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = TASK_WIDTH;
+ taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = TASK_HEIGHT;
+ mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true);
+
+ verify(mWindowManager, never()).inflateLayout();
}
@Test
@@ -364,7 +379,6 @@ public class CompatUIWindowManagerTest extends ShellTestCase {
// Create button if it is not created.
mWindowManager.mLayout = null;
mWindowManager.mHasSizeCompat = true;
- doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(mTaskInfo);
mWindowManager.updateVisibility(/* canShow= */ true);
verify(mWindowManager).createLayout(/* canShow= */ true);
@@ -489,7 +503,6 @@ public class CompatUIWindowManagerTest extends ShellTestCase {
TaskInfo taskInfo = createTaskInfo(true, CAMERA_COMPAT_CONTROL_HIDDEN);
taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 2000, 2000));
taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 2000;
- taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1850;
assertFalse(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo));
@@ -514,6 +527,11 @@ public class CompatUIWindowManagerTest extends ShellTestCase {
taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat;
taskInfo.appCompatTaskInfo.cameraCompatControlState = cameraCompatControlState;
taskInfo.configuration.uiMode &= ~Configuration.UI_MODE_TYPE_DESK;
+ // Letterboxed activity that takes half the screen should show size compat restart button
+ taskInfo.configuration.windowConfiguration.setBounds(
+ new Rect(0, 0, TASK_WIDTH, TASK_HEIGHT));
+ taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 1000;
+ taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1000;
return taskInfo;
}
}
diff --git a/media/java/android/media/RoutingSessionInfo.java b/media/java/android/media/RoutingSessionInfo.java
index d28c26df6749..2202766ef016 100644
--- a/media/java/android/media/RoutingSessionInfo.java
+++ b/media/java/android/media/RoutingSessionInfo.java
@@ -182,7 +182,7 @@ public final class RoutingSessionInfo implements Parcelable {
mControlHints = src.readBundle();
mIsSystemSession = src.readBoolean();
mTransferReason = src.readInt();
- mTransferInitiatorUserHandle = src.readParcelable(null, android.os.UserHandle.class);
+ mTransferInitiatorUserHandle = UserHandle.readFromParcel(src);
mTransferInitiatorPackageName = src.readString();
}
@@ -417,11 +417,7 @@ public final class RoutingSessionInfo implements Parcelable {
dest.writeBundle(mControlHints);
dest.writeBoolean(mIsSystemSession);
dest.writeInt(mTransferReason);
- if (mTransferInitiatorUserHandle != null) {
- mTransferInitiatorUserHandle.writeToParcel(dest, /* flags= */ 0);
- } else {
- dest.writeParcelable(null, /* flags= */ 0);
- }
+ UserHandle.writeToParcel(mTransferInitiatorUserHandle, dest);
dest.writeString(mTransferInitiatorPackageName);
}
diff --git a/packages/SettingsLib/res/drawable/ic_external_display.xml b/packages/SettingsLib/res/drawable/ic_external_display.xml
new file mode 100644
index 000000000000..de50de8c07c8
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_external_display.xml
@@ -0,0 +1,28 @@
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="25dp"
+ android:viewportWidth="24"
+ android:viewportHeight="25">
+ <group>
+ <clip-path
+ android:pathData="M0,0.307h24v24h-24z"/>
+ <path
+ android:pathData="M8,21.307V19.307H10V17.307H4C3.45,17.307 2.975,17.115 2.575,16.732C2.192,16.332 2,15.857 2,15.307V5.307C2,4.757 2.192,4.29 2.575,3.907C2.975,3.507 3.45,3.307 4,3.307H20C20.55,3.307 21.017,3.507 21.4,3.907C21.8,4.29 22,4.757 22,5.307V15.307C22,15.857 21.8,16.332 21.4,16.732C21.017,17.115 20.55,17.307 20,17.307H14V19.307H16V21.307H8ZM4,15.307H20V5.307H4V15.307ZM4,15.307V5.307V15.307Z"
+ android:fillColor="#E5E3D6"/>
+ </group>
+</vector>
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java b/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java
index cf224dc3be5f..3de49336f427 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java
@@ -71,15 +71,15 @@ public class DeviceIconUtil {
new Device(
AudioDeviceInfo.TYPE_HDMI,
MediaRoute2Info.TYPE_HDMI,
- mIsTv ? R.drawable.ic_tv : R.drawable.ic_headphone),
+ mIsTv ? R.drawable.ic_tv : R.drawable.ic_external_display),
new Device(
AudioDeviceInfo.TYPE_HDMI_ARC,
MediaRoute2Info.TYPE_HDMI_ARC,
- mIsTv ? R.drawable.ic_hdmi : R.drawable.ic_headphone),
+ mIsTv ? R.drawable.ic_hdmi : R.drawable.ic_external_display),
new Device(
AudioDeviceInfo.TYPE_HDMI_EARC,
MediaRoute2Info.TYPE_HDMI_EARC,
- mIsTv ? R.drawable.ic_hdmi : R.drawable.ic_headphone),
+ mIsTv ? R.drawable.ic_hdmi : R.drawable.ic_external_display),
new Device(
AudioDeviceInfo.TYPE_WIRED_HEADSET,
MediaRoute2Info.TYPE_WIRED_HEADSET,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt
index 6e3b15da4423..c643238b7e30 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt
@@ -52,8 +52,8 @@ class ConversationNotificationProcessor @Inject constructor(
entry: NotificationEntry,
recoveredBuilder: Notification.Builder,
logger: NotificationContentInflaterLogger
- ) {
- val messagingStyle = recoveredBuilder.style as? Notification.MessagingStyle ?: return
+ ): Notification.MessagingStyle? {
+ val messagingStyle = recoveredBuilder.style as? Notification.MessagingStyle ?: return null
messagingStyle.conversationType =
if (entry.ranking.channel.isImportantConversation)
Notification.MessagingStyle.CONVERSATION_TYPE_IMPORTANT
@@ -68,6 +68,7 @@ class ConversationNotificationProcessor @Inject constructor(
}
messagingStyle.unreadMessageCount =
conversationNotificationManager.getUnreadCount(entry, recoveredBuilder)
+ return messagingStyle
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
index 73decfc326a4..639e23ae0765 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
@@ -362,8 +362,12 @@ public class PreparationCoordinator implements Coordinator {
}
NotifInflater.Params getInflaterParams(NotifUiAdjustment adjustment, String reason) {
- return new NotifInflater.Params(adjustment.isMinimized(), reason,
- adjustment.isSnoozeEnabled());
+ return new NotifInflater.Params(
+ adjustment.isMinimized(),
+ reason,
+ adjustment.isSnoozeEnabled(),
+ adjustment.isChildInGroup()
+ );
}
private void abortInflation(NotificationEntry entry, String reason) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt
index 4483599d6857..c0b187be42f3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt
@@ -20,9 +20,9 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.render.NotifViewController
/**
- * Used by the [PreparationCoordinator]. When notifications are added or updated, the
- * NotifInflater is asked to (re)inflated and prepare their views. This inflation occurs off the
- * main thread. When the inflation is finished, NotifInflater will trigger its InflationCallback.
+ * Used by the [PreparationCoordinator]. When notifications are added or updated, the NotifInflater
+ * is asked to (re)inflated and prepare their views. This inflation occurs off the main thread. When
+ * the inflation is finished, NotifInflater will trigger its InflationCallback.
*/
interface NotifInflater {
/**
@@ -33,7 +33,7 @@ interface NotifInflater {
fun rebindViews(entry: NotificationEntry, params: Params, callback: InflationCallback)
/**
- * Called to inflate the views of an entry. Views are not considered inflated until all of its
+ * Called to inflate the views of an entry. Views are not considered inflated until all of its
* views are bound. Once all views are inflated, the InflationCallback is triggered.
*
* @param callback callback called after inflation finishes
@@ -41,25 +41,24 @@ interface NotifInflater {
fun inflateViews(entry: NotificationEntry, params: Params, callback: InflationCallback)
/**
- * Request to stop the inflation of an entry. For example, called when a notification is
- * removed and no longer needs to be inflated. Returns whether anything may have been aborted.
+ * Request to stop the inflation of an entry. For example, called when a notification is removed
+ * and no longer needs to be inflated. Returns whether anything may have been aborted.
*/
fun abortInflation(entry: NotificationEntry): Boolean
- /**
- * Called to let the system remove the content views from the notification row.
- */
+ /** Called to let the system remove the content views from the notification row. */
fun releaseViews(entry: NotificationEntry)
- /**
- * Callback once all the views are inflated and bound for a given NotificationEntry.
- */
+ /** Callback once all the views are inflated and bound for a given NotificationEntry. */
interface InflationCallback {
fun onInflationFinished(entry: NotificationEntry, controller: NotifViewController)
}
- /**
- * A class holding parameters used when inflating the notification row
- */
- class Params(val isLowPriority: Boolean, val reason: String, val showSnooze: Boolean)
+ /** A class holding parameters used when inflating the notification row */
+ class Params(
+ val isLowPriority: Boolean,
+ val reason: String,
+ val showSnooze: Boolean,
+ val isChildInGroup: Boolean = false,
+ )
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt
index ee0b00807e27..e1d2cdc65d5a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt
@@ -20,6 +20,7 @@ import android.app.Notification
import android.app.RemoteInput
import android.graphics.drawable.Icon
import android.text.TextUtils
+import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation
/**
* An immutable object which contains minimal state extracted from an entry that represents state
@@ -34,6 +35,7 @@ class NotifUiAdjustment internal constructor(
val isSnoozeEnabled: Boolean,
val isMinimized: Boolean,
val needsRedaction: Boolean,
+ val isChildInGroup: Boolean,
) {
companion object {
@JvmStatic
@@ -48,6 +50,11 @@ class NotifUiAdjustment internal constructor(
oldAdjustment.needsRedaction != newAdjustment.needsRedaction -> true
areDifferent(oldAdjustment.smartActions, newAdjustment.smartActions) -> true
newAdjustment.smartReplies != oldAdjustment.smartReplies -> true
+ // TODO(b/217799515): Here we decide whether to re-inflate the row on every group-status
+ // change if we want to keep the single-line view, the following line should be:
+ // !oldAdjustment.isChildInGroup && newAdjustment.isChildInGroup -> true
+ AsyncHybridViewInflation.isEnabled &&
+ oldAdjustment.isChildInGroup != newAdjustment.isChildInGroup -> true
else -> false
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt
index 058545689c01..6f44c13a3e71 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt
@@ -29,6 +29,7 @@ import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
+import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager
import com.android.systemui.util.ListenerSet
import com.android.systemui.util.settings.SecureSettings
import javax.inject.Inject
@@ -43,7 +44,8 @@ class NotifUiAdjustmentProvider @Inject constructor(
private val secureSettings: SecureSettings,
private val lockscreenUserManager: NotificationLockscreenUserManager,
private val sectionStyleProvider: SectionStyleProvider,
- private val userTracker: UserTracker
+ private val userTracker: UserTracker,
+ private val groupMembershipManager: GroupMembershipManager,
) {
private val dirtyListeners = ListenerSet<Runnable>()
private var isSnoozeSettingsEnabled = false
@@ -121,5 +123,6 @@ class NotifUiAdjustmentProvider @Inject constructor(
isSnoozeEnabled = isSnoozeSettingsEnabled && !entry.isCanceled,
isMinimized = isEntryMinimized(entry),
needsRedaction = lockscreenUserManager.needsRedaction(entry),
+ isChildInGroup = groupMembershipManager.isChildInGroup(entry),
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
index 80ef14bb4673..cd816aea452b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
@@ -20,6 +20,7 @@ import static com.android.systemui.Flags.screenshareNotificationHiding;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC;
+import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_SINGLE_LINE;
import static java.util.Objects.requireNonNull;
@@ -49,6 +50,7 @@ import com.android.systemui.statusbar.notification.row.RowContentBindParams;
import com.android.systemui.statusbar.notification.row.RowContentBindStage;
import com.android.systemui.statusbar.notification.row.RowInflaterTask;
import com.android.systemui.statusbar.notification.row.dagger.ExpandableNotificationRowComponent;
+import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
import javax.inject.Inject;
@@ -127,6 +129,8 @@ public class NotificationRowBinderImpl implements NotificationRowBinder {
@NonNull NotifInflater.Params params,
NotificationRowContentBinder.InflationCallback callback)
throws InflationException {
+ //TODO(b/217799515): Remove the entry parameter from getViewParentForNotification(), this
+ // function returns the NotificationStackScrollLayout regardless of the entry.
ViewGroup parent = mListContainer.getViewParentForNotification(entry);
if (entry.rowExists()) {
@@ -174,6 +178,9 @@ public class NotificationRowBinderImpl implements NotificationRowBinder {
params.markContentViewsFreeable(FLAG_CONTENT_VIEW_CONTRACTED);
params.markContentViewsFreeable(FLAG_CONTENT_VIEW_EXPANDED);
params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC);
+ if (AsyncHybridViewInflation.isEnabled()) {
+ params.markContentViewsFreeable(FLAG_CONTENT_VIEW_SINGLE_LINE);
+ }
mRowContentBindStage.requestRebind(entry, null);
}
@@ -254,6 +261,16 @@ public class NotificationRowBinderImpl implements NotificationRowBinder {
params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC);
}
+ if (AsyncHybridViewInflation.isEnabled()) {
+ if (inflaterParams.isChildInGroup()) {
+ params.requireContentViews(FLAG_CONTENT_VIEW_SINGLE_LINE);
+ } else {
+ // TODO(b/217799515): here we decide whether to free the single-line view
+ // when the group status changes
+ params.markContentViewsFreeable(FLAG_CONTENT_VIEW_SINGLE_LINE);
+ }
+ }
+
params.rebindAllContentViews();
mLogger.logRequestingRebind(entry, inflaterParams);
mRowContentBindStage.requestRebind(entry, en -> {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt
index 61e6f65b2bc2..8021d8f58ccc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt
@@ -127,6 +127,9 @@ class ShadeViewDiffer(
}
}
+ /**
+ * Attach the Child Nodes to the parentNode using the structure from specMap
+ */
private fun attachChildren(
parentNode: ShadeNode,
specMap: Map<NodeController, NodeSpec>
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java
index d626c18e46f5..8ae324fa4ef8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java
@@ -44,6 +44,7 @@ public abstract class BindStage<Params> extends BindRequester {
/**
* Execute the stage asynchronously.
*
+ * @param entry the NotificationEntry to bind
* @param row notification top-level view to bind views to
* @param callback callback after stage finishes
*/
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java
index 43d99a0e03f2..6bc2b2f9e250 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java
@@ -16,19 +16,27 @@
package com.android.systemui.statusbar.notification.row;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
+import android.content.res.ColorStateList;
import android.graphics.drawable.Icon;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
+import android.view.ViewStub;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.widget.ConversationLayout;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.notification.NotificationFadeAware;
+import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.ConversationAvatar;
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.FacePile;
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleIcon;
/**
* A hybrid view which may contain information about one ore more conversations.
@@ -37,6 +45,7 @@ public class HybridConversationNotificationView extends HybridNotificationView {
private ImageView mConversationIconView;
private TextView mConversationSenderName;
+ private ViewStub mConversationFacePileStub;
private View mConversationFacePile;
private int mSingleAvatarSize;
private int mFacePileSize;
@@ -65,7 +74,16 @@ public class HybridConversationNotificationView extends HybridNotificationView {
protected void onFinishInflate() {
super.onFinishInflate();
mConversationIconView = requireViewById(com.android.internal.R.id.conversation_icon);
- mConversationFacePile = requireViewById(com.android.internal.R.id.conversation_face_pile);
+ if (AsyncHybridViewInflation.isEnabled()) {
+ mConversationFacePileStub =
+ requireViewById(com.android.internal.R.id.conversation_face_pile);
+ } else {
+ // TODO(b/217799515): This usage is vague because mConversationFacePile represents both
+ // View and ViewStub at different stages of View inflation, should be removed when
+ // AsyncHybridViewInflation flag is removed
+ mConversationFacePile =
+ requireViewById(com.android.internal.R.id.conversation_face_pile);
+ }
mConversationSenderName = requireViewById(R.id.conversation_notification_sender);
applyTextColor(mConversationSenderName, mSecondaryTextColor);
mFacePileSize = getResources()
@@ -85,7 +103,8 @@ public class HybridConversationNotificationView extends HybridNotificationView {
@Override
public void bind(@Nullable CharSequence title, @Nullable CharSequence text,
- @Nullable View contentView) {
+ @Nullable View contentView) {
+ AsyncHybridViewInflation.assertInLegacyMode();
if (!(contentView instanceof ConversationLayout)) {
super.bind(title, text, contentView);
return;
@@ -137,6 +156,77 @@ public class HybridConversationNotificationView extends HybridNotificationView {
super.bind(conversationTitle, conversationText, conversationLayout);
}
+ /**
+ * Set the avatar using ConversationAvatar from SingleLineViewModel
+ *
+ * @param conversationAvatar the icon needed for a single-line conversation view, it should be
+ * either an instance of SingleIcon or FacePile
+ */
+ public void setAvatar(@NonNull ConversationAvatar conversationAvatar) {
+ if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return;
+ if (conversationAvatar instanceof SingleIcon) {
+ SingleIcon avatar = (SingleIcon) conversationAvatar;
+ if (mConversationFacePile != null) mConversationFacePile.setVisibility(GONE);
+ mConversationIconView.setVisibility(VISIBLE);
+ mConversationIconView.setImageDrawable(avatar.getIconDrawable());
+ setSize(mConversationIconView, mSingleAvatarSize);
+ return;
+ }
+
+ // If conversationAvatar is not a SingleIcon, it should be a FacePile.
+ // Bind the face pile with it.
+ FacePile facePileModel = (FacePile) conversationAvatar;
+ mConversationIconView.setVisibility(GONE);
+ // Inflate mConversationFacePile from ViewStub
+ if (mConversationFacePile == null) {
+ mConversationFacePile = mConversationFacePileStub.inflate();
+ }
+ mConversationFacePile.setVisibility(VISIBLE);
+
+ ImageView facePileBottomBg = mConversationFacePile.requireViewById(
+ com.android.internal.R.id.conversation_face_pile_bottom_background);
+ ImageView facePileBottom = mConversationFacePile.requireViewById(
+ com.android.internal.R.id.conversation_face_pile_bottom);
+ ImageView facePileTop = mConversationFacePile.requireViewById(
+ com.android.internal.R.id.conversation_face_pile_top);
+
+ int bottomBackgroundColor = facePileModel.getBottomBackgroundColor();
+ facePileBottomBg.setImageTintList(ColorStateList.valueOf(bottomBackgroundColor));
+
+ facePileBottom.setImageDrawable(facePileModel.getBottomIconDrawable());
+ facePileTop.setImageDrawable(facePileModel.getTopIconDrawable());
+
+ setSize(mConversationFacePile, mFacePileSize);
+ setSize(facePileBottom, mFacePileAvatarSize);
+ setSize(facePileTop, mFacePileAvatarSize);
+ setSize(facePileBottomBg, mFacePileAvatarSize + 2 * mFacePileProtectionWidth);
+
+ mTransformationHelper.addViewTransformingToSimilar(facePileTop);
+ mTransformationHelper.addViewTransformingToSimilar(facePileBottom);
+ mTransformationHelper.addViewTransformingToSimilar(facePileBottomBg);
+
+ }
+
+ /**
+ * bind the text views
+ */
+ public void setText(
+ CharSequence titleText,
+ CharSequence contentText,
+ CharSequence conversationSenderName
+ ) {
+ if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return;
+ if (conversationSenderName == null) {
+ mConversationSenderName.setVisibility(GONE);
+ } else {
+ mConversationSenderName.setVisibility(VISIBLE);
+ mConversationSenderName.setText(conversationSenderName);
+ }
+ // TODO (b/217799515): super.bind() doesn't use contentView, remove the contentView
+ // argument when the flag is removed
+ super.bind(/* title = */ titleText, /* text = */ contentText, /* contentView = */ null);
+ }
+
private static void setSize(View view, int size) {
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) view.getLayoutParams();
lp.width = size;
@@ -153,4 +243,9 @@ public class HybridConversationNotificationView extends HybridNotificationView {
super.setNotificationFaded(faded);
NotificationFadeAware.setLayerTypeForFaded(mConversationFacePile, faded);
}
+
+ @VisibleForTesting
+ TextView getConversationSenderNameView() {
+ return mConversationSenderName;
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java
index ddd9bddc7375..09c034978977 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java
@@ -32,6 +32,7 @@ import android.widget.TextView;
import com.android.internal.widget.ConversationLayout;
import com.android.systemui.res.R;
+import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
/**
* A class managing hybrid groups that include {@link HybridNotificationView} and the notification
@@ -41,6 +42,8 @@ public class HybridGroupManager {
private final Context mContext;
+ private static final String TAG = "HybridGroupManager";
+
private float mOverflowNumberSize;
private int mOverflowNumberPadding;
@@ -93,21 +96,34 @@ public class HybridGroupManager {
public HybridNotificationView bindFromNotification(HybridNotificationView reusableView,
View contentView, StatusBarNotification notification,
ViewGroup parent) {
+ AsyncHybridViewInflation.assertInLegacyMode();
boolean isNewView = false;
if (reusableView == null) {
Trace.beginSection("HybridGroupManager#bindFromNotification");
reusableView = inflateHybridView(contentView, parent);
isNewView = true;
}
- CharSequence titleText = resolveTitle(notification.getNotification());
- CharSequence contentText = resolveText(notification.getNotification());
- reusableView.bind(titleText, contentText, contentView);
+
+ updateReusableView(reusableView, notification, contentView);
if (isNewView) {
Trace.endSection();
}
return reusableView;
}
+ /**
+ * Update the HybridNotificationView (single-line view)'s appearance
+ */
+ public void updateReusableView(HybridNotificationView reusableView,
+ StatusBarNotification notification, View contentView) {
+ AsyncHybridViewInflation.assertInLegacyMode();
+ final CharSequence titleText = resolveTitle(notification.getNotification());
+ final CharSequence contentText = resolveText(notification.getNotification());
+ if (reusableView != null) {
+ reusableView.bind(titleText, contentText, contentView);
+ }
+ }
+
@Nullable
public static CharSequence resolveText(Notification notification) {
CharSequence contentText = notification.extras.getCharSequence(Notification.EXTRA_TEXT);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index f186e665f773..913d5f6d3848 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -20,6 +20,7 @@ import static com.android.internal.annotations.VisibleForTesting.Visibility.PACK
import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED;
import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED;
import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP;
+import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_SINGLELINE;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -41,15 +42,19 @@ import android.widget.RemoteViews;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.widget.ImageMessageConsumer;
-import com.android.systemui.res.R;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.media.controls.util.MediaFeatureFlag;
+import com.android.systemui.res.R;
import com.android.systemui.statusbar.InflationTask;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.notification.ConversationNotificationProcessor;
import com.android.systemui.statusbar.notification.InflationException;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
+import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineConversationViewBinder;
+import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineViewBinder;
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleLineViewModel;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.systemui.statusbar.policy.InflatedSmartReplyState;
@@ -135,7 +140,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder
AsyncInflationTask task = new AsyncInflationTask(
mBgExecutor,
mInflateSynchronously,
- contentToBind,
+ /* reInflateFlags = */ contentToBind,
mRemoteViewCache,
entry,
mConversationProcessor,
@@ -145,7 +150,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder
bindParams.usesIncreasedHeadsUpHeight,
callback,
mRemoteInputManager.getRemoteViewsOnClickHandler(),
- mIsMediaInQS,
+ /* isMediaFlagEnabled = */ mIsMediaInQS,
mSmartReplyStateInflater,
mNotifLayoutInflaterFactoryProvider,
mLogger);
@@ -178,6 +183,29 @@ public class NotificationContentInflater implements NotificationRowContentBinder
result = inflateSmartReplyViews(result, reInflateFlags, entry, row.getContext(),
packageContext, row.getExistingSmartReplyState(), smartRepliesInflater, mLogger);
+ if (AsyncHybridViewInflation.isEnabled()) {
+ boolean isConversation = entry.getRanking().isConversation();
+ Notification.MessagingStyle messagingStyle = null;
+ if (isConversation) {
+ messagingStyle = mConversationProcessor
+ .processNotification(entry, builder, mLogger);
+ }
+ result.mInflatedSingleLineViewModel = SingleLineViewInflater
+ .inflateSingleLineViewModel(
+ entry.getSbn().getNotification(),
+ messagingStyle,
+ builder,
+ row.getContext()
+ );
+ result.mInflatedSingleLineViewHolder =
+ SingleLineViewInflater.inflateSingleLineViewHolder(
+ isConversation,
+ reInflateFlags,
+ entry,
+ row.getContext(),
+ mLogger
+ );
+ }
apply(
mBgExecutor,
@@ -255,6 +283,15 @@ public class NotificationContentInflater implements NotificationRowContentBinder
mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC);
});
break;
+ case FLAG_CONTENT_VIEW_SINGLE_LINE: {
+ if (AsyncHybridViewInflation.isEnabled()) {
+ row.getPrivateLayout().performWhenContentInactive(
+ VISIBLE_TYPE_SINGLELINE,
+ () -> row.getPrivateLayout().setSingleLineView(null)
+ );
+ }
+ break;
+ }
default:
break;
}
@@ -282,6 +319,10 @@ public class NotificationContentInflater implements NotificationRowContentBinder
if ((contentViews & FLAG_CONTENT_VIEW_PUBLIC) != 0) {
row.getPublicLayout().removeContentInactiveRunnable(VISIBLE_TYPE_CONTRACTED);
}
+ if (AsyncHybridViewInflation.isEnabled()
+ && (contentViews & FLAG_CONTENT_VIEW_SINGLE_LINE) != 0) {
+ row.getPrivateLayout().removeContentInactiveRunnable(VISIBLE_TYPE_SINGLELINE);
+ }
}
private static InflationProgress inflateSmartReplyViews(
@@ -772,6 +813,25 @@ public class NotificationContentInflater implements NotificationRowContentBinder
}
setRepliesAndActions = true;
}
+
+ if (AsyncHybridViewInflation.isEnabled()
+ && (reInflateFlags & FLAG_CONTENT_VIEW_SINGLE_LINE) != 0) {
+ HybridNotificationView viewHolder = result.mInflatedSingleLineViewHolder;
+ SingleLineViewModel viewModel = result.mInflatedSingleLineViewModel;
+ if (viewHolder != null && viewModel != null) {
+ if (viewModel.isConversation()) {
+ SingleLineConversationViewBinder.bind(
+ result.mInflatedSingleLineViewModel,
+ result.mInflatedSingleLineViewHolder
+ );
+ } else {
+ SingleLineViewBinder.bind(result.mInflatedSingleLineViewModel,
+ result.mInflatedSingleLineViewHolder);
+ }
+ privateLayout.setSingleLineView(result.mInflatedSingleLineViewHolder);
+ }
+ }
+
if (setRepliesAndActions) {
privateLayout.setInflatedSmartReplyState(result.inflatedSmartReplyState);
}
@@ -941,19 +1001,23 @@ public class NotificationContentInflater implements NotificationRowContentBinder
// For all of our templates, we want it to be RTL
packageContext = new RtlEnabledContext(packageContext);
}
- if (mEntry.getRanking().isConversation()) {
- mConversationProcessor.processNotification(mEntry, recoveredBuilder, mLogger);
+ boolean isConversation = mEntry.getRanking().isConversation();
+ Notification.MessagingStyle messagingStyle = null;
+ if (isConversation) {
+ messagingStyle = mConversationProcessor.processNotification(
+ mEntry, recoveredBuilder, mLogger);
}
InflationProgress inflationProgress = createRemoteViews(mReInflateFlags,
recoveredBuilder, mIsLowPriority, mUsesIncreasedHeight,
mUsesIncreasedHeadsUpHeight, packageContext, mRow,
mNotifLayoutInflaterFactoryProvider, mLogger);
+
mLogger.logAsyncTaskProgress(mEntry,
"getting existing smart reply state (on wrong thread!)");
InflatedSmartReplyState previousSmartReplyState = mRow.getExistingSmartReplyState();
mLogger.logAsyncTaskProgress(mEntry, "inflating smart reply views");
InflationProgress result = inflateSmartReplyViews(
- inflationProgress,
+ /* result = */ inflationProgress,
mReInflateFlags,
mEntry,
mContext,
@@ -962,6 +1026,27 @@ public class NotificationContentInflater implements NotificationRowContentBinder
mSmartRepliesInflater,
mLogger);
+ if (AsyncHybridViewInflation.isEnabled()) {
+ // Inflate the single-line content view's ViewModel and ViewHolder from the
+ // background thread, the ViewHolder needs to be bind with ViewModel later from
+ // the main thread.
+ result.mInflatedSingleLineViewModel = SingleLineViewInflater
+ .inflateSingleLineViewModel(
+ mEntry.getSbn().getNotification(),
+ messagingStyle,
+ recoveredBuilder,
+ mContext
+ );
+ result.mInflatedSingleLineViewHolder =
+ SingleLineViewInflater.inflateSingleLineViewHolder(
+ isConversation,
+ mReInflateFlags,
+ mEntry,
+ mContext,
+ mLogger
+ );
+ }
+
mLogger.logAsyncTaskProgress(mEntry,
"getting row image resolver (on wrong thread!)");
final NotificationInlineImageResolver imageResolver = mRow.getImageResolver();
@@ -1078,6 +1163,11 @@ public class NotificationContentInflater implements NotificationRowContentBinder
private InflatedSmartReplyState inflatedSmartReplyState;
private InflatedSmartReplyViewHolder expandedInflatedSmartReplies;
private InflatedSmartReplyViewHolder headsUpInflatedSmartReplies;
+
+ // ViewModel for SingleLineView, holds the UI State
+ SingleLineViewModel mInflatedSingleLineViewModel;
+ // Inflated SingleLineViewHolder, SingleLineView that lacks the UI State
+ HybridNotificationView mInflatedSingleLineViewHolder;
}
@VisibleForTesting
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterLogger.kt
index 4f5455dc455f..ee9462c60674 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterLogger.kt
@@ -26,6 +26,7 @@ import com.android.systemui.statusbar.notification.row.NotificationRowContentBin
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC
+import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_SINGLE_LINE
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag
import javax.inject.Inject
@@ -99,6 +100,26 @@ constructor(@NotifInflationLog private val buffer: LogBuffer) {
)
}
+ fun logInflateSingleLine(
+ entry: NotificationEntry,
+ @InflationFlag inflationFlags: Int,
+ isConversation: Boolean
+ ) {
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ {
+ str1 = entry.logKey
+ int1 = inflationFlags
+ bool1 = isConversation
+ },
+ {
+ "inflateSingleLineView, inflationFlags: ${flagToString(int1)} for $str1, " +
+ "isConversation: $bool1"
+ }
+ )
+ }
+
companion object {
fun flagToString(@InflationFlag flag: Int): String {
if (flag == 0) {
@@ -121,6 +142,9 @@ constructor(@NotifInflationLog private val buffer: LogBuffer) {
if (flag and FLAG_CONTENT_VIEW_PUBLIC != 0) {
l.add("PUBLIC")
}
+ if (flag and FLAG_CONTENT_VIEW_SINGLE_LINE != 0) {
+ l.add("SINGLE_LINE")
+ }
return l.joinToString("|")
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index a1718b9fbb02..402ea51bebb6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -57,6 +57,7 @@ import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
+import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationCustomViewWrapper;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
import com.android.systemui.statusbar.policy.InflatedSmartReplyState;
@@ -86,7 +87,7 @@ public class NotificationContentView extends FrameLayout implements Notification
public static final int VISIBLE_TYPE_CONTRACTED = 0;
public static final int VISIBLE_TYPE_EXPANDED = 1;
public static final int VISIBLE_TYPE_HEADSUP = 2;
- private static final int VISIBLE_TYPE_SINGLELINE = 3;
+ public static final int VISIBLE_TYPE_SINGLELINE = 3;
/**
* Used when there is no content on the view such as when we're a public layout but don't
* need to show.
@@ -98,6 +99,7 @@ public class NotificationContentView extends FrameLayout implements Notification
private final Rect mClipBounds = new Rect();
private int mMinContractedHeight;
+ private int mMinSingleLineHeight;
private View mContractedChild;
private View mExpandedChild;
private View mHeadsUpChild;
@@ -234,6 +236,11 @@ public class NotificationContentView extends FrameLayout implements Notification
public void reinflate() {
mMinContractedHeight = getResources().getDimensionPixelSize(
R.dimen.min_notification_layout_height);
+ if (AsyncHybridViewInflation.isEnabled()) {
+ //TODO: set the height with a more reasonable min single-line height
+ mMinSingleLineHeight = getResources().getDimensionPixelSize(
+ R.dimen.conversation_single_line_face_pile_size);
+ }
}
public void setHeights(int smallHeight, int headsUpMaxHeight, int maxHeight) {
@@ -540,6 +547,28 @@ public class NotificationContentView extends FrameLayout implements Notification
updateShownWrapper(mVisibleType);
}
+ /**
+ * Sets the single-line view. Child may be null to remove the view.
+ * @param child single-line content view to set
+ */
+ public void setSingleLineView(@Nullable HybridNotificationView child) {
+ if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return;
+ if (mSingleLineView != null) {
+ mOnContentViewInactiveListeners.remove(mSingleLineView);
+ mSingleLineView.animate().cancel();
+ removeView(mSingleLineView);
+ }
+ if (child == null) {
+ mSingleLineView = null;
+ if (mTransformationStartVisibleType == VISIBLE_TYPE_SINGLELINE) {
+ mTransformationStartVisibleType = VISIBLE_TYPE_NONE;
+ }
+ return;
+ }
+ addView(child);
+ mSingleLineView = child;
+ }
+
@Override
public void onViewAdded(View child) {
super.onViewAdded(child);
@@ -809,7 +838,17 @@ public class NotificationContentView extends FrameLayout implements Notification
return mContractedChild != null
? getViewHeight(VISIBLE_TYPE_CONTRACTED) : mMinContractedHeight;
} else {
- return mSingleLineView.getHeight();
+ if (AsyncHybridViewInflation.isEnabled()) {
+ if (mSingleLineView != null) {
+ return getViewHeight(VISIBLE_TYPE_SINGLELINE);
+ } else {
+ Log.wtf(TAG, "getMinHeight: mSingleLineView == null");
+ return mMinSingleLineHeight;
+ }
+ } else {
+ AsyncHybridViewInflation.assertInLegacyMode();
+ return mSingleLineView.getHeight();
+ }
}
}
@@ -1264,19 +1303,30 @@ public class NotificationContentView extends FrameLayout implements Notification
}
private void updateSingleLineView() {
- if (mIsChildInGroup) {
+ try {
Trace.beginSection("NotifContentView#updateSingleLineView");
- boolean isNewView = mSingleLineView == null;
- mSingleLineView = mHybridGroupManager.bindFromNotification(
- mSingleLineView, mContractedChild, mNotificationEntry.getSbn(), this);
- if (isNewView) {
- updateViewVisibility(mVisibleType, VISIBLE_TYPE_SINGLELINE,
- mSingleLineView, mSingleLineView);
+ if (AsyncHybridViewInflation.isEnabled()) {
+ return;
+ }
+ AsyncHybridViewInflation.assertInLegacyMode();
+ if (mIsChildInGroup) {
+ boolean isNewView = mSingleLineView == null;
+ mSingleLineView = mHybridGroupManager.bindFromNotification(
+ /* reusableView = */ mSingleLineView,
+ /* contentView = */ mContractedChild,
+ /* notification = */ mNotificationEntry.getSbn(),
+ /* parent = */ this
+ );
+ if (isNewView && mSingleLineView != null) {
+ updateViewVisibility(mVisibleType, VISIBLE_TYPE_SINGLELINE,
+ mSingleLineView, mSingleLineView);
+ }
+ } else if (mSingleLineView != null) {
+ removeView(mSingleLineView);
+ mSingleLineView = null;
}
+ } finally {
Trace.endSection();
- } else if (mSingleLineView != null) {
- removeView(mSingleLineView);
- mSingleLineView = null;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java
index d7b7aa210257..736140c44dfd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java
@@ -80,6 +80,7 @@ public interface NotificationRowContentBinder {
FLAG_CONTENT_VIEW_EXPANDED,
FLAG_CONTENT_VIEW_HEADS_UP,
FLAG_CONTENT_VIEW_PUBLIC,
+ FLAG_CONTENT_VIEW_SINGLE_LINE,
FLAG_CONTENT_VIEW_ALL})
@interface InflationFlag {}
/**
@@ -102,7 +103,12 @@ public interface NotificationRowContentBinder {
*/
int FLAG_CONTENT_VIEW_PUBLIC = 1 << 3;
- int FLAG_CONTENT_VIEW_ALL = (1 << 4) - 1;
+ /**
+ * The single line notification view. Show when the notification is shown as a child in group.
+ */
+ int FLAG_CONTENT_VIEW_SINGLE_LINE = 1 << 4;
+
+ int FLAG_CONTENT_VIEW_ALL = (1 << 5) - 1;
/**
* Parameters for content view binding
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java
index a52f638e7c26..1494c275d061 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java
@@ -102,9 +102,9 @@ public final class RowContentBindParams {
* @see InflationFlag
*/
public void markContentViewsFreeable(@InflationFlag int contentViews) {
- @InflationFlag int existingContentViews = contentViews &= mContentViews;
+ @InflationFlag int existingFreeableContentViews = contentViews &= mContentViews;
mContentViews &= ~contentViews;
- mDirtyContentViews |= existingContentViews;
+ mDirtyContentViews |= existingFreeableContentViews;
}
public @InflationFlag int getContentViews() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java
index b70da00ad517..f4f8374d0a9f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java
@@ -63,7 +63,10 @@ public class RowContentBindStage extends BindStage<RowContentBindParams> {
@InflationFlag int inflationFlags = params.getContentViews();
@InflationFlag int invalidatedFlags = params.getDirtyContentViews();
+ // Rebind the content views which are needed now, and the corresponding old views are
+ // invalidated
@InflationFlag int contentToBind = invalidatedFlags & inflationFlags;
+ // Unbind the content views that are not needed
@InflationFlag int contentToUnbind = inflationFlags ^ FLAG_CONTENT_VIEW_ALL;
// Bind/unbind with parameters
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt
new file mode 100644
index 000000000000..d6118a0b3865
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt
@@ -0,0 +1,390 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import android.app.Notification
+import android.app.Notification.MessagingStyle
+import android.app.Person
+import android.content.Context
+import android.graphics.drawable.Icon
+import android.util.Log
+import android.view.LayoutInflater
+import com.android.app.tracing.traceSection
+import com.android.internal.R
+import com.android.internal.widget.MessagingMessage
+import com.android.internal.widget.PeopleHelper
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.logKey
+import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_SINGLE_LINE
+import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.ConversationAvatar
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.ConversationData
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.FacePile
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleIcon
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleLineViewModel
+
+/** The inflater of SingleLineViewModel and SingleLineViewHolder */
+internal object SingleLineViewInflater {
+ const val TAG = "SingleLineViewInflater"
+
+ /**
+ * Inflate an instance of SingleLineViewModel.
+ *
+ * @param notification the notification to show
+ * @param messagingStyle the MessagingStyle information is only provided for conversation
+ * notification, not for legacy messaging notifications
+ * @param builder the recovered Notification Builder
+ * @param systemUiContext the context of Android System UI
+ * @return the inflated SingleLineViewModel
+ */
+ @JvmStatic
+ fun inflateSingleLineViewModel(
+ notification: Notification,
+ messagingStyle: MessagingStyle?,
+ builder: Notification.Builder,
+ systemUiContext: Context,
+ ): SingleLineViewModel {
+ if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) {
+ return SingleLineViewModel(null, null, null)
+ }
+ peopleHelper.init(systemUiContext)
+ var titleText = HybridGroupManager.resolveTitle(notification)
+ var contentText = HybridGroupManager.resolveText(notification)
+
+ if (messagingStyle == null) {
+ return SingleLineViewModel(
+ titleText = titleText,
+ contentText = contentText,
+ conversationData = null,
+ )
+ }
+
+ val isGroupConversation = messagingStyle.isGroupConversation
+
+ val conversationTextData = messagingStyle.loadConversationTextData(systemUiContext)
+ if (conversationTextData?.conversationTitle?.isNotEmpty() == true) {
+ titleText = conversationTextData.conversationTitle
+ }
+ if (conversationTextData?.conversationText?.isNotEmpty() == true) {
+ contentText = conversationTextData.conversationText
+ }
+
+ val conversationAvatar =
+ messagingStyle.loadConversationAvatar(
+ notification = notification,
+ isGroupConversation = isGroupConversation,
+ builder = builder,
+ systemUiContext = systemUiContext
+ )
+
+ val conversationData =
+ ConversationData(
+ // We don't show the sender's name for one-to-one conversation
+ conversationSenderName =
+ if (isGroupConversation) conversationTextData?.senderName else null,
+ avatar = conversationAvatar
+ )
+
+ return SingleLineViewModel(
+ titleText = titleText,
+ contentText = contentText,
+ conversationData = conversationData,
+ )
+ }
+
+ /** load conversation text data from the MessagingStyle of conversation notifications */
+ private fun MessagingStyle.loadConversationTextData(
+ systemUiContext: Context
+ ): ConversationTextData? {
+ if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) {
+ return null
+ }
+ var conversationText: CharSequence?
+
+ if (messages.isEmpty()) {
+ return null
+ }
+
+ // load the conversation text
+ val lastMessage = messages[messages.lastIndex]
+ conversationText = lastMessage.text
+ if (conversationText == null && lastMessage.isImageMessage()) {
+ conversationText = findBackUpConversationText(lastMessage, systemUiContext)
+ }
+
+ // load the sender's name to display
+ val name = lastMessage.senderPerson?.name
+ val senderName =
+ systemUiContext.resources.getString(
+ R.string.conversation_single_line_name_display,
+ name
+ )
+
+ // We need to find back-up values for those texts if they are needed and empty
+ return ConversationTextData(
+ conversationTitle = conversationTitle
+ ?: findBackUpConversationTitle(senderName, systemUiContext),
+ conversationText = conversationText,
+ senderName = senderName,
+ )
+ }
+
+ private fun MessagingStyle.Message.isImageMessage(): Boolean = MessagingMessage.hasImage(this)
+
+ /** find a back-up conversation title when the conversation title is null. */
+ private fun MessagingStyle.findBackUpConversationTitle(
+ senderName: CharSequence?,
+ systemUiContext: Context,
+ ): CharSequence {
+ if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) {
+ return ""
+ }
+ return if (isGroupConversation) {
+ systemUiContext.resources.getString(R.string.conversation_title_fallback_group_chat)
+ } else {
+ // Is one-to-one, let's try to use the last sender's name
+ // The last back-up is the value of resource: conversation_title_fallback_one_to_one
+ senderName
+ ?: systemUiContext.resources.getString(
+ R.string.conversation_title_fallback_one_to_one
+ )
+ }
+ }
+
+ /**
+ * find a back-up conversation text when the conversation has null text and is image message.
+ */
+ private fun findBackUpConversationText(
+ message: MessagingStyle.Message,
+ context: Context,
+ ): CharSequence? {
+ if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) {
+ return null
+ }
+ // If the message is not an image message, just return empty, the back-up text for showing
+ // will be SingleLineViewModel.contentText
+ if (!message.isImageMessage()) return null
+ // If is image message, return a placeholder
+ return context.resources.getString(R.string.conversation_single_line_image_placeholder)
+ }
+
+ /**
+ * The text data that we load from a conversation notification to show in the single-line views.
+ *
+ * Group conversation single-line view should be formatted as:
+ * [conversationTitle, senderName, conversationText]
+ *
+ * One-to-one single-line view should be formatted as:
+ * [conversationTitle (which is equal to the senderName), conversationText]
+ *
+ * @property conversationTitle the title of the conversation, not necessarily the title of the
+ * notification row. conversationTitle is non-null, though may be empty, in which case we need
+ * to show the notification title instead.
+ * @property conversationText the text content of the conversation, single-line will use the
+ * notification's text when conversationText is null
+ * @property senderName the sender's name to be shown in the row when needed. senderName can be
+ * null
+ */
+ data class ConversationTextData(
+ val conversationTitle: CharSequence,
+ val conversationText: CharSequence?,
+ val senderName: CharSequence?,
+ )
+
+ private fun groupMessages(
+ messages: List<MessagingStyle.Message>,
+ historicMessages: List<MessagingStyle.Message>,
+ ): List<MutableList<MessagingStyle.Message>> {
+ if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) {
+ return listOf()
+ }
+ if (messages.isEmpty() && historicMessages.isEmpty()) return listOf()
+ var currentGroup: MutableList<MessagingStyle.Message>? = null
+ var currentSenderKey: CharSequence? = null
+ val groups = mutableListOf<MutableList<MessagingStyle.Message>>()
+ for (i in 0 until (historicMessages.size + messages.size)) {
+ val message = if (i < historicMessages.size) historicMessages[i] else messages[i]
+
+ val sender = message.senderPerson
+ val senderKey = sender?.getKeyOrName()
+ val isNewGroup = (currentGroup == null) || senderKey != currentSenderKey
+ if (isNewGroup) {
+ currentGroup = mutableListOf()
+ groups.add(currentGroup)
+ currentSenderKey = senderKey
+ }
+ currentGroup?.add(message)
+ }
+ return groups
+ }
+
+ private fun MessagingStyle.loadConversationAvatar(
+ builder: Notification.Builder,
+ notification: Notification,
+ isGroupConversation: Boolean,
+ systemUiContext: Context,
+ ): ConversationAvatar {
+ if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) {
+ return SingleIcon(null)
+ }
+ val userKey = user.getKeyOrName()
+ var conversationIcon: Icon? = null
+ var conversationText: CharSequence? = conversationTitle
+
+ val groups = groupMessages(messages, historicMessages)
+ val uniqueNames = peopleHelper.mapUniqueNamesToPrefixWithGroupList(groups)
+
+ if (!isGroupConversation) {
+ // Conversation is one-to-one, load the single icon
+ // Let's resolve the icon / text from the last sender
+ if (shortcutIcon != null) {
+ conversationIcon = shortcutIcon
+ }
+
+ for (i in messages.lastIndex downTo 0) {
+ val message = messages[i]
+ val sender = message.senderPerson
+ val senderKey = sender?.getKeyOrName()
+ if ((sender != null && senderKey != userKey) || i == 0) {
+ if (conversationText.isNullOrEmpty()) {
+ // We use the senderName as header text if no conversation title is provided
+ // (This usually happens for most 1:1 conversations)
+ conversationText = sender?.name ?: ""
+ }
+ if (conversationIcon == null) {
+ var avatarIcon = sender?.icon
+ if (avatarIcon == null) {
+ avatarIcon = builder.getDefaultAvatar(name = conversationText)
+ }
+ conversationIcon = avatarIcon
+ }
+ break
+ }
+ }
+ }
+
+ if (conversationIcon == null) {
+ conversationIcon = notification.getLargeIcon()
+ }
+
+ // If is one-to-one or the conversation has an icon, return a single icon
+ if (!isGroupConversation || conversationIcon != null) {
+ return SingleIcon(conversationIcon?.loadDrawable(systemUiContext))
+ }
+
+ // Otherwise, let's find the two last conversations to build a face pile:
+ var secondLastIcon: Icon? = null
+ var lastIcon: Icon? = null
+ var lastKey: CharSequence? = null
+
+ for (i in groups.lastIndex downTo 0) {
+ val message = groups[i][0]
+ val sender = message.senderPerson ?: user
+ val senderKey = sender.getKeyOrName()
+ val notUser = senderKey != userKey
+ val notIncluded = senderKey != lastKey
+
+ if ((notUser && notIncluded) || (i == 0 && lastKey == null)) {
+ if (lastIcon == null) {
+ lastIcon =
+ sender.icon
+ ?: builder.getDefaultAvatar(
+ name = sender.name,
+ uniqueNames = uniqueNames
+ )
+ lastKey = senderKey
+ } else {
+ secondLastIcon =
+ sender.icon
+ ?: builder.getDefaultAvatar(
+ name = sender.name,
+ uniqueNames = uniqueNames
+ )
+ break
+ }
+ }
+ }
+
+ if (lastIcon == null) {
+ lastIcon = builder.getDefaultAvatar(name = "")
+ }
+
+ if (secondLastIcon == null) {
+ secondLastIcon = builder.getDefaultAvatar(name = "")
+ }
+
+ return FacePile(
+ topIconDrawable = secondLastIcon.loadDrawable(systemUiContext),
+ bottomIconDrawable = lastIcon.loadDrawable(systemUiContext),
+ bottomBackgroundColor = builder.getBackgroundColor(/* isHeader = */ false),
+ )
+ }
+
+ @JvmStatic
+ fun inflateSingleLineViewHolder(
+ isConversation: Boolean,
+ reinflateFlags: Int,
+ entry: NotificationEntry,
+ context: Context,
+ logger: NotificationContentInflaterLogger,
+ ): HybridNotificationView? {
+ if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return null
+ if (reinflateFlags and FLAG_CONTENT_VIEW_SINGLE_LINE == 0) {
+ return null
+ }
+
+ logger.logInflateSingleLine(entry, reinflateFlags, isConversation)
+ logger.logAsyncTaskProgress(entry, "inflating single-line content view")
+
+ var view: HybridNotificationView? = null
+
+ traceSection("NotificationContentInflater#inflateSingleLineView") {
+ val inflater = LayoutInflater.from(context)
+ val layoutRes: Int =
+ if (isConversation)
+ com.android.systemui.res.R.layout.hybrid_conversation_notification
+ else com.android.systemui.res.R.layout.hybrid_notification
+ view = inflater.inflate(layoutRes, /* root = */ null) as HybridNotificationView
+ if (view == null) {
+ Log.wtf(TAG, "Single-line view inflation result is null for entry: ${entry.logKey}")
+ }
+ }
+ return view
+ }
+
+ private fun Notification.Builder.getDefaultAvatar(
+ name: CharSequence?,
+ uniqueNames: PeopleHelper.NameToPrefixMap? = null
+ ): Icon {
+ val layoutColor = getSmallIconColor(/* isHeader = */ false)
+ if (!name.isNullOrEmpty()) {
+ val symbol = uniqueNames?.getPrefix(name) ?: ""
+ return peopleHelper.createAvatarSymbol(
+ /* name = */ name,
+ /* symbol = */ symbol,
+ /* layoutColor = */ layoutColor
+ )
+ }
+ // If name is null, create default avatar with background color
+ // TODO(b/319829062): Investigate caching default icon for color
+ return peopleHelper.createAvatarSymbol(/* name = */ "", /* symbol = */ "", layoutColor)
+ }
+
+ private fun Person.getKeyOrName(): CharSequence? = if (key == null) name else key
+
+ private val peopleHelper = PeopleHelper()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineConversationViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineConversationViewBinder.kt
new file mode 100644
index 000000000000..69284bd7ef48
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineConversationViewBinder.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.ui.viewbinder
+
+import com.android.systemui.statusbar.notification.row.HybridConversationNotificationView
+import com.android.systemui.statusbar.notification.row.HybridNotificationView
+import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleLineViewModel
+
+object SingleLineConversationViewBinder {
+ @JvmStatic
+ fun bind(viewModel: SingleLineViewModel, view: HybridNotificationView?) {
+ if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return
+ if (view !is HybridConversationNotificationView || !viewModel.isConversation()) {
+ SingleLineViewBinder.bind(viewModel, view)
+ return
+ }
+
+ viewModel.conversationData?.avatar?.let { view.setAvatar(it) }
+ view.setText(
+ viewModel.titleText,
+ viewModel.contentText,
+ viewModel.conversationData?.conversationSenderName
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineViewBinder.kt
new file mode 100644
index 000000000000..22e10c165521
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineViewBinder.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.ui.viewbinder
+
+import com.android.systemui.statusbar.notification.row.HybridNotificationView
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleLineViewModel
+
+object SingleLineViewBinder {
+ @JvmStatic
+ fun bind(viewModel: SingleLineViewModel?, view: HybridNotificationView?) {
+ // bind the title and content text views
+ view?.apply {
+ bind(
+ /* title = */ viewModel?.titleText,
+ /* text = */ viewModel?.contentText,
+ /* contentView = */ null
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/SingleLineViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/SingleLineViewModel.kt
new file mode 100644
index 000000000000..d583fa5d97ed
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/SingleLineViewModel.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.ui.viewmodel
+
+import android.annotation.ColorInt
+import android.graphics.drawable.Drawable
+
+/**
+ * ViewModel for SingleLine Notification View.
+ *
+ * @property titleText the text of notification view title
+ * @property contentText the text of view content
+ * @property conversationData the data that is needed specifically for conversation single-line
+ * views. Null conversationData shows that the notification is not conversation. Legacy
+ * MessagingStyle Notifications doesn't have this member.
+ */
+data class SingleLineViewModel(
+ var titleText: CharSequence?,
+ var contentText: CharSequence?,
+ var conversationData: ConversationData?,
+) {
+ fun isConversation(): Boolean {
+ return conversationData != null
+ }
+}
+
+/**
+ * @property conversationSenderName the name of sender to show in the single-line view. Only group
+ * conversation single-line views show the sender name.
+ * @property avatar the avatar to show for the conversation
+ */
+data class ConversationData(
+ val conversationSenderName: CharSequence?,
+ val avatar: ConversationAvatar,
+)
+
+/**
+ * An avatar to show for a single-line conversation notification, it can be either a single icon or
+ * a face pile.
+ */
+sealed class ConversationAvatar
+
+data class SingleIcon(val iconDrawable: Drawable?) : ConversationAvatar()
+
+/**
+ * A kind of avatar to show for a group conversation notification view. It consists of two avatars
+ * of the last two senders.
+ */
+data class FacePile(
+ val topIconDrawable: Drawable?,
+ val bottomIconDrawable: Drawable?,
+ @ColorInt val bottomBackgroundColor: Int
+) : ConversationAvatar()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index 45b9c269b61c..abf6c27c68ac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -1295,8 +1295,8 @@ public class NotificationChildrenContainer extends ViewGroup
if (singleLineView != null) {
minExpandHeight += singleLineView.getHeight();
} else {
- Log.e(TAG, "getMinHeight: child " + child + " single line view is null",
- new Exception());
+ Log.e(TAG, "getMinHeight: child " + child.getEntry().getKey()
+ + " single line view is null", new Exception());
}
visibleChildren++;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
index f842e304ffdf..fe5bdd41a94f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -18,9 +18,12 @@ package com.android.systemui.statusbar.notification.stack.ui.viewbinder
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
+import android.view.View
+import android.view.WindowInsets
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
@@ -30,6 +33,9 @@ import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificat
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
/** Binds the shared notification container to its view-model. */
@@ -65,6 +71,8 @@ object SharedNotificationContainerBinder {
}
}
+ val burnInParams = MutableStateFlow(BurnInParameters())
+
/*
* For animation sensitive coroutines, immediately run just like applicationScope does
* instead of doing a post() to the main thread. This extra delay can cause visible jitter.
@@ -122,7 +130,11 @@ object SharedNotificationContainerBinder {
}
}
- launch { viewModel.translationY.collect { controller.setTranslationY(it) } }
+ launch {
+ burnInParams
+ .flatMapLatest { params -> viewModel.translationY(params) }
+ .collect { y -> controller.setTranslationY(y) }
+ }
launch {
viewModel.expansionAlpha.collect { controller.setMaxAlphaForExpansion(it) }
@@ -137,11 +149,20 @@ object SharedNotificationContainerBinder {
controller.setOnHeightChangedRunnable(Runnable { viewModel.notificationStackChanged() })
+ view.setOnApplyWindowInsetsListener { v: View, insets: WindowInsets ->
+ val insetTypes = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout()
+ burnInParams.update { current ->
+ current.copy(topInset = insets.getInsetsIgnoringVisibility(insetTypes).top)
+ }
+ insets
+ }
+
return object : DisposableHandle {
override fun dispose() {
disposableHandle.dispose()
disposableHandleMainImmediate.dispose()
controller.setOnHeightChangedRunnable(null)
+ view.setOnApplyWindowInsetsListener(null)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index 99cd89b84c14..4617ce49f44a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -28,6 +28,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED
import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
+import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
+import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGlanceableHubTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel
@@ -65,10 +67,11 @@ constructor(
keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val shadeInteractor: ShadeInteractor,
communalInteractor: CommunalInteractor,
- occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
+ private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
lockscreenToOccludedTransitionViewModel: LockscreenToOccludedTransitionViewModel,
glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel,
- lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel
+ lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel,
+ private val aodBurnInViewModel: AodBurnInViewModel,
) {
private val statesForConstrainedNotifications =
setOf(
@@ -313,20 +316,22 @@ constructor(
* Under certain scenarios, such as swiping up on the lockscreen, the container will need to be
* translated as the keyguard fades out.
*/
- val translationY: Flow<Float> =
- combine(
+ fun translationY(params: BurnInParameters): Flow<Float> {
+ return combine(
+ aodBurnInViewModel.translationY(params).onStart { emit(0f) },
isOnLockscreenWithoutShade,
merge(
keyguardInteractor.keyguardTranslationY,
occludedToLockscreenTransitionViewModel.lockscreenTranslationY,
)
- ) { isOnLockscreenWithoutShade, translationY ->
+ ) { burnInY, isOnLockscreenWithoutShade, translationY ->
if (isOnLockscreenWithoutShade) {
- translationY
+ burnInY + translationY
} else {
0f
}
}
+ }
/**
* When on keyguard, there is limited space to display notifications so calculate how many could
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
index 58eec2e71d32..4519ba6d3590 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
@@ -65,6 +65,7 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider;
+import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
import com.android.systemui.statusbar.notification.collection.render.NotifViewBarn;
import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager;
import com.android.systemui.util.settings.SecureSettings;
@@ -111,6 +112,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase {
@Spy private FakeNotifInflater mNotifInflater = new FakeNotifInflater();
private final SectionStyleProvider mSectionStyleProvider = new SectionStyleProvider();
@Mock private UserTracker mUserTracker;
+ @Mock private GroupMembershipManager mGroupMembershipManager;
private NotifUiAdjustmentProvider mAdjustmentProvider;
@@ -127,7 +129,9 @@ public class PreparationCoordinatorTest extends SysuiTestCase {
mSecureSettings,
mLockscreenUserManager,
mSectionStyleProvider,
- mUserTracker);
+ mUserTracker,
+ mGroupMembershipManager
+ );
mEntry = getNotificationEntryBuilder().setParent(ROOT_ENTRY).build();
mInflationError = new Exception(TEST_MESSAGE);
mErrorManager = new NotifInflationErrorManager();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt
index f9f8d8a2cfc6..73c49c023dd5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt
@@ -17,6 +17,8 @@ package com.android.systemui.statusbar.notification.collection.inflation
import android.database.ContentObserver
import android.os.Handler
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.provider.Settings.Secure.SHOW_NOTIFICATION_SNOOZE
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
@@ -28,6 +30,8 @@ import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
+import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager
+import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
@@ -35,6 +39,8 @@ import com.android.systemui.util.mockito.withArgCaptor
import com.android.systemui.util.settings.FakeSettings
import com.android.systemui.util.settings.SecureSettings
import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -55,6 +61,7 @@ class NotifUiAdjustmentProviderTest : SysuiTestCase() {
private val uri = FakeSettings().getUriFor(SHOW_NOTIFICATION_SNOOZE)
private val dirtyListener: Runnable = mock()
private val userTracker: UserTracker = mock()
+ private val groupMembershipManager: GroupMembershipManager = mock()
private val section = NotifSection(mock(), 0)
private val entry = NotificationEntryBuilder()
@@ -69,7 +76,8 @@ class NotifUiAdjustmentProviderTest : SysuiTestCase() {
secureSettings,
lockscreenUserManager,
sectionStyleProvider,
- userTracker
+ userTracker,
+ groupMembershipManager,
)
@Before
@@ -127,4 +135,42 @@ class NotifUiAdjustmentProviderTest : SysuiTestCase() {
assertThat(withSnoozing.isSnoozeEnabled).isTrue()
assertThat(withSnoozing).isNotEqualTo(original)
}
+
+ @Test
+ @EnableFlags(AsyncHybridViewInflation.FLAG_NAME)
+ fun changeIsChildInGroup_asyncHybirdFlagEnabled_needReInflation() {
+ // Given: an Entry that is not child in group
+ // AsyncHybridViewInflation flag is enabled
+ whenever(groupMembershipManager.isChildInGroup(entry)).thenReturn(false)
+ val oldAdjustment = adjustmentProvider.calculateAdjustment(entry)
+ assertThat(oldAdjustment.isChildInGroup).isFalse()
+
+ // When: the Entry becomes a group child
+ whenever(groupMembershipManager.isChildInGroup(entry)).thenReturn(true)
+ val newAdjustment = adjustmentProvider.calculateAdjustment(entry)
+ assertThat(newAdjustment.isChildInGroup).isTrue()
+ assertThat(newAdjustment).isNotEqualTo(oldAdjustment)
+
+ // Then: need re-inflation
+ assertTrue(NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment))
+ }
+
+ @Test
+ @DisableFlags(AsyncHybridViewInflation.FLAG_NAME)
+ fun changeIsChildInGroup_asyncHybirdFlagDisabled_noNeedForReInflation() {
+ // Given: an Entry that is not child in group
+ // AsyncHybridViewInflation flag is disabled
+ whenever(groupMembershipManager.isChildInGroup(entry)).thenReturn(false)
+ val oldAdjustment = adjustmentProvider.calculateAdjustment(entry)
+ assertThat(oldAdjustment.isChildInGroup).isFalse()
+
+ // When: the Entry becomes a group child
+ whenever(groupMembershipManager.isChildInGroup(entry)).thenReturn(true)
+ val newAdjustment = adjustmentProvider.calculateAdjustment(entry)
+ assertThat(newAdjustment.isChildInGroup).isTrue()
+ assertThat(newAdjustment).isNotEqualTo(oldAdjustment)
+
+ // Then: need no re-inflation
+ assertFalse(NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment))
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
index b0996ad48d0a..a0d10759ba56 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
@@ -88,6 +88,8 @@ public class NotificationContentInflaterTest extends SysuiTestCase {
private Notification.Builder mBuilder;
private ExpandableNotificationRow mRow;
+ private NotificationTestHelper mHelper;
+
@Mock private NotifRemoteViewCache mCache;
@Mock private ConversationNotificationProcessor mConversationNotificationProcessor;
@Mock private InflatedSmartReplyState mInflatedSmartReplyState;
@@ -119,11 +121,11 @@ public class NotificationContentInflaterTest extends SysuiTestCase {
.setContentTitle("Title")
.setContentText("Text")
.setStyle(new Notification.BigTextStyle().bigText("big text"));
- NotificationTestHelper helper = new NotificationTestHelper(
+ mHelper = new NotificationTestHelper(
mContext,
mDependency,
TestableLooper.get(this));
- ExpandableNotificationRow row = helper.createRow(mBuilder.build());
+ ExpandableNotificationRow row = mHelper.createRow(mBuilder.build());
mRow = spy(row);
when(mNotifLayoutInflaterFactoryProvider.provide(any(), any()))
.thenReturn(mNotifLayoutInflaterFactory);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineConversationViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineConversationViewBinderTest.kt
new file mode 100644
index 000000000000..1c959af6ec3f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineConversationViewBinderTest.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.row
+
+import android.app.Notification
+import android.app.Person
+import android.platform.test.annotations.EnableFlags
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.internal.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_SINGLE_LINE
+import com.android.systemui.statusbar.notification.row.SingleLineViewInflater.inflateSingleLineViewHolder
+import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation
+import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineConversationViewBinder
+import com.android.systemui.util.mockito.mock
+import kotlin.test.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class SingleLineConversationViewBinderTest : SysuiTestCase() {
+ private lateinit var notificationBuilder: Notification.Builder
+ private lateinit var helper: NotificationTestHelper
+
+ @Before
+ fun setUp() {
+ allowTestableLooperAsMainThread()
+ helper = NotificationTestHelper(context, mDependency, TestableLooper.get(this))
+ notificationBuilder = Notification.Builder(context, CHANNEL_ID)
+ notificationBuilder
+ .setSmallIcon(R.drawable.ic_corp_icon)
+ .setContentTitle(CONTENT_TITLE)
+ .setContentText(CONTENT_TEXT)
+ }
+
+ @Test
+ @EnableFlags(AsyncHybridViewInflation.FLAG_NAME)
+ fun bindGroupConversationSingleLineView() {
+ // GIVEN a row with a group conversation notification
+ val user =
+ Person.Builder()
+ // .setIcon(Icon.createWithResource(mContext,
+ // R.drawable.ic_account_circle))
+ .setName(USER_NAME)
+ .build()
+ val style =
+ Notification.MessagingStyle(user)
+ .addMessage(MESSAGE_TEXT, System.currentTimeMillis(), user)
+ .addMessage(
+ "How about lunch?",
+ System.currentTimeMillis(),
+ Person.Builder().setName("user2").build()
+ )
+ .setGroupConversation(true)
+ notificationBuilder.setStyle(style).setShortcutId(SHORTCUT_ID)
+ val notification = notificationBuilder.build()
+ val row = helper.createRow(notification)
+
+ val viewHolder =
+ inflateSingleLineViewHolder(
+ isConversation = true,
+ reinflateFlags = FLAG_CONTENT_VIEW_SINGLE_LINE,
+ entry = row.entry,
+ context = context,
+ logger = mock()
+ )
+ as HybridConversationNotificationView
+ val viewModel =
+ SingleLineViewInflater.inflateSingleLineViewModel(
+ notification = notification,
+ messagingStyle = style,
+ builder = notificationBuilder,
+ systemUiContext = context,
+ )
+ // WHEN: binds the viewHolder
+ SingleLineConversationViewBinder.bind(
+ viewModel,
+ viewHolder,
+ )
+
+ // THEN: the single-line conversation view should be bind with view model's corresponding
+ // fields
+ assertEquals(viewModel.titleText, viewHolder.titleView.text)
+ assertEquals(viewModel.contentText, viewHolder.textView.text)
+ assertEquals(
+ viewModel.conversationData?.conversationSenderName,
+ viewHolder.conversationSenderNameView.text
+ )
+ }
+
+ private companion object {
+ const val CHANNEL_ID = "CHANNEL_ID"
+ const val CONTENT_TITLE = "CONTENT_TITLE"
+ const val CONTENT_TEXT = "CONTENT_TEXT"
+ const val USER_NAME = "USER_NAME"
+ const val MESSAGE_TEXT = "MESSAGE_TEXT"
+ const val SHORTCUT_ID = "Shortcut"
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt
new file mode 100644
index 000000000000..f0fc349777b2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.row
+
+import android.app.Notification
+import android.platform.test.annotations.EnableFlags
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.internal.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_SINGLE_LINE
+import com.android.systemui.statusbar.notification.row.SingleLineViewInflater.inflateSingleLineViewHolder
+import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation
+import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineViewBinder
+import com.android.systemui.util.mockito.mock
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class SingleLineViewBinderTest : SysuiTestCase() {
+ private lateinit var notificationBuilder: Notification.Builder
+ private lateinit var helper: NotificationTestHelper
+
+ @Before
+ fun setUp() {
+ allowTestableLooperAsMainThread()
+ helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
+ notificationBuilder = Notification.Builder(mContext, CHANNEL_ID)
+ notificationBuilder
+ .setSmallIcon(R.drawable.ic_corp_icon)
+ .setContentTitle(CONTENT_TITLE)
+ .setContentText(CONTENT_TEXT)
+ }
+
+ @Test
+ @EnableFlags(AsyncHybridViewInflation.FLAG_NAME)
+ fun bindNonConversationSingleLineView() {
+ // GIVEN: a row with bigText style notification
+ val style = Notification.BigTextStyle().bigText(CONTENT_TEXT)
+ notificationBuilder.setStyle(style)
+ val notification = notificationBuilder.build()
+ val row: ExpandableNotificationRow = helper.createRow(notification)
+
+ val viewHolder =
+ inflateSingleLineViewHolder(
+ isConversation = false,
+ reinflateFlags = FLAG_CONTENT_VIEW_SINGLE_LINE,
+ entry = row.entry,
+ context = context,
+ logger = mock()
+ )
+ val viewModel =
+ SingleLineViewInflater.inflateSingleLineViewModel(
+ notification = notification,
+ messagingStyle = null,
+ builder = notificationBuilder,
+ systemUiContext = context,
+ )
+
+ // WHEN: binds the viewHolder
+ SingleLineViewBinder.bind(viewModel, viewHolder)
+
+ // THEN: the single-line view should be bind with viewModel's title and content text
+ Assert.assertEquals(viewModel.titleText, viewHolder?.titleView?.text)
+ Assert.assertEquals(viewModel.contentText, viewHolder?.textView?.text)
+ }
+
+ private companion object {
+ const val CHANNEL_ID = "CHANNEL_ID"
+ const val CONTENT_TITLE = "A Cool New Feature"
+ const val CONTENT_TEXT = "Checkout out new feature!"
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt
new file mode 100644
index 000000000000..b67153a842ac
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt
@@ -0,0 +1,463 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import android.app.Notification
+import android.app.Person
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffXfermode
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.Icon
+import android.platform.test.annotations.EnableFlags
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.core.graphics.drawable.toBitmap
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.ConversationAvatar
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.FacePile
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleIcon
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleLineViewModel
+import kotlin.test.assertEquals
+import kotlin.test.assertIs
+import kotlin.test.assertIsNot
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+@EnableFlags(AsyncHybridViewInflation.FLAG_NAME)
+class SingleLineViewInflaterTest : SysuiTestCase() {
+ private lateinit var helper: NotificationTestHelper
+ // Non-group MessagingStyles only have firstSender
+ private lateinit var firstSender: Person
+ private lateinit var lastSender: Person
+ private lateinit var firstSenderIcon: Icon
+ private lateinit var lastSenderIcon: Icon
+ private var firstSenderIconDrawable: Drawable? = null
+ private var lastSenderIconDrawable: Drawable? = null
+ private val currentUser: Person? = null
+
+ private companion object {
+ const val FIRST_SENDER_NAME = "First Sender"
+ const val LAST_SENDER_NAME = "Second Sender"
+ const val LAST_MESSAGE = "How about lunch?"
+
+ const val CONVERSATION_TITLE = "The Sender Family"
+ const val CONTENT_TITLE = "A Cool Group"
+ const val CONTENT_TEXT = "This is an amazing group chat"
+
+ const val SHORTCUT_ID = "Shortcut"
+ }
+
+ @Before
+ fun setUp() {
+ helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
+ firstSenderIcon = Icon.createWithBitmap(getBitmap(context, R.drawable.ic_person))
+ firstSenderIconDrawable = firstSenderIcon.loadDrawable(context)
+ lastSenderIcon =
+ Icon.createWithBitmap(
+ getBitmap(context, com.android.internal.R.drawable.ic_account_circle)
+ )
+ lastSenderIconDrawable = lastSenderIcon.loadDrawable(context)
+ firstSender = Person.Builder().setName(FIRST_SENDER_NAME).setIcon(firstSenderIcon).build()
+ lastSender = Person.Builder().setName(LAST_SENDER_NAME).setIcon(lastSenderIcon).build()
+ }
+
+ @Test
+ fun createViewModelForNonConversationSingleLineView() {
+ // Given: a non-conversation notification
+ val notificationType = NonMessaging()
+ val notification = getNotification(NonMessaging())
+
+ // When: inflate the SingleLineViewModel
+ val singleLineViewModel = notification.makeSingleLineViewModel(notificationType)
+
+ // Then: the inflated SingleLineViewModel should be as expected
+ // conversationData: null, because it's not a conversation notification
+ assertEquals(SingleLineViewModel(CONTENT_TITLE, CONTENT_TEXT, null), singleLineViewModel)
+ }
+
+ @Test
+ fun createViewModelForNonGroupConversationNotification() {
+ // Given: a non-group conversation notification
+ val notificationType = OneToOneConversation()
+ val notification = getNotification(notificationType)
+
+ // When: inflate the SingleLineViewModel
+ val singleLineViewModel = notification.makeSingleLineViewModel(notificationType)
+
+ // Then: the inflated SingleLineViewModel should be as expected
+ // titleText: Notification.ConversationTitle
+ // contentText: the last message text
+ // conversationSenderName: null, because it's not a group conversation
+ // conversationData.avatar: a single icon of the last sender
+ assertEquals(CONVERSATION_TITLE, singleLineViewModel.titleText)
+ assertEquals(LAST_MESSAGE, singleLineViewModel.contentText)
+ assertNull(
+ singleLineViewModel.conversationData?.conversationSenderName,
+ "Sender name should be null for one-on-one conversation"
+ )
+ assertTrue {
+ singleLineViewModel.conversationData
+ ?.avatar
+ ?.equalsTo(SingleIcon(firstSenderIcon.loadDrawable(context))) == true
+ }
+ }
+
+ @Test
+ fun createViewModelForNonGroupLegacyMessagingStyleNotification() {
+ // Given: a non-group legacy messaging style notification
+ val notificationType = LegacyMessaging()
+ val notification = getNotification(notificationType)
+
+ // When: inflate the SingleLineViewModel
+ val singleLineViewModel = notification.makeSingleLineViewModel(notificationType)
+
+ // Then: the inflated SingleLineViewModel should be as expected
+ // titleText: CONVERSATION_TITLE: SENDER_NAME
+ // contentText: the last message text
+ // conversationData: null, because it's not a conversation notification
+ assertEquals("$CONVERSATION_TITLE: $FIRST_SENDER_NAME", singleLineViewModel.titleText)
+ assertEquals(LAST_MESSAGE, singleLineViewModel.contentText)
+ assertNull(
+ singleLineViewModel.conversationData,
+ "conversationData should be null for legacy messaging conversation"
+ )
+ }
+
+ @Test
+ fun createViewModelForGroupLegacyMessagingStyleNotification() {
+ // Given: a non-group legacy messaging style notification
+ val notificationType = LegacyMessagingGroup()
+ val notification = getNotification(notificationType)
+
+ // When: inflate the SingleLineViewModel
+ val singleLineViewModel = notification.makeSingleLineViewModel(notificationType)
+
+ // Then: the inflated SingleLineViewModel should be as expected
+ // titleText: CONVERSATION_TITLE: LAST_SENDER_NAME
+ // contentText: the last message text
+ // conversationData: null, because it's not a conversation notification
+ assertEquals("$CONVERSATION_TITLE: $LAST_SENDER_NAME", singleLineViewModel.titleText)
+ assertEquals(LAST_MESSAGE, singleLineViewModel.contentText)
+ assertNull(
+ singleLineViewModel.conversationData,
+ "conversationData should be null for legacy messaging conversation"
+ )
+ }
+
+ @Test
+ fun createViewModelForNonGroupConversationNotificationWithShortcutIcon() {
+ // Given: a non-group conversation notification with a shortcut icon
+ val shortcutIcon =
+ Icon.createWithResource(context, com.android.internal.R.drawable.ic_account_circle)
+ val notificationType = OneToOneConversation(shortcutIcon = shortcutIcon)
+ val notification = getNotification(notificationType)
+
+ // When: inflate the SingleLineViewModel
+ val singleLineViewModel = notification.makeSingleLineViewModel(notificationType)
+
+ // Then: the inflated SingleLineViewModel should be expected
+ // titleText: Notification.ConversationTitle
+ // contentText: the last message text
+ // conversationSenderName: null, because it's not a group conversation
+ // conversationData.avatar: a single icon of the shortcut icon
+ assertEquals(CONVERSATION_TITLE, singleLineViewModel.titleText)
+ assertEquals(LAST_MESSAGE, singleLineViewModel.contentText)
+ assertNull(
+ singleLineViewModel.conversationData?.conversationSenderName,
+ "Sender name should be null for one-on-one conversation"
+ )
+ assertTrue {
+ singleLineViewModel.conversationData
+ ?.avatar
+ ?.equalsTo(SingleIcon(shortcutIcon.loadDrawable(context))) == true
+ }
+ }
+
+ @Test
+ fun createViewModelForGroupConversationNotificationWithLargeIcon() {
+ // Given: a group conversation notification with a large icon
+ val largeIcon =
+ Icon.createWithResource(context, com.android.internal.R.drawable.ic_account_circle)
+ val notificationType = GroupConversation(largeIcon = largeIcon)
+ val notification = getNotification(notificationType)
+
+ // When: inflate the SingleLineViewModel
+ val singleLineViewModel = notification.makeSingleLineViewModel(notificationType)
+
+ // Then: the inflated SingleLineViewModel should be expected
+ // titleText: Notification.ConversationTitle
+ // contentText: the last message text
+ // conversationSenderName: the last non-user sender's name
+ // conversationData.avatar: a single icon
+ assertEquals(CONVERSATION_TITLE, singleLineViewModel.titleText)
+ assertEquals(LAST_MESSAGE, singleLineViewModel.contentText)
+ assertEquals(
+ context.resources.getString(
+ com.android.internal.R.string.conversation_single_line_name_display,
+ LAST_SENDER_NAME
+ ),
+ singleLineViewModel.conversationData?.conversationSenderName
+ )
+ assertTrue {
+ singleLineViewModel.conversationData
+ ?.avatar
+ ?.equalsTo(SingleIcon(largeIcon.loadDrawable(context))) == true
+ }
+ }
+
+ @Test
+ fun createViewModelForGroupConversationWithNoIcon() {
+ // Given: a group conversation notification
+ val notificationType = GroupConversation()
+ val notification = getNotification(notificationType)
+
+ // When: inflate the SingleLineViewModel
+ val singleLineViewModel = notification.makeSingleLineViewModel(notificationType)
+
+ // Then: the inflated SingleLineViewModel should be expected
+ // titleText: Notification.ConversationTitle
+ // contentText: the last message text
+ // conversationSenderName: the last non-user sender's name
+ // conversationData.avatar: a face-pile consists the last sender's icon
+ assertEquals(CONVERSATION_TITLE, singleLineViewModel.titleText)
+ assertEquals(LAST_MESSAGE, singleLineViewModel.contentText)
+ assertEquals(
+ context.resources.getString(
+ com.android.internal.R.string.conversation_single_line_name_display,
+ LAST_SENDER_NAME
+ ),
+ singleLineViewModel.conversationData?.conversationSenderName
+ )
+
+ val backgroundColor =
+ Notification.Builder.recoverBuilder(context, notification)
+ .getBackgroundColor(/* isHeader = */ false)
+ assertTrue {
+ singleLineViewModel.conversationData
+ ?.avatar
+ ?.equalsTo(
+ FacePile(
+ firstSenderIconDrawable,
+ lastSenderIconDrawable,
+ backgroundColor,
+ )
+ ) == true
+ }
+ }
+
+ sealed class NotificationType(val largeIcon: Icon? = null)
+
+ class NonMessaging(largeIcon: Icon? = null) : NotificationType(largeIcon)
+
+ class LegacyMessaging(largeIcon: Icon? = null) : NotificationType(largeIcon)
+
+ class LegacyMessagingGroup(largeIcon: Icon? = null) : NotificationType(largeIcon)
+
+ class OneToOneConversation(largeIcon: Icon? = null, val shortcutIcon: Icon? = null) :
+ NotificationType(largeIcon)
+
+ class GroupConversation(largeIcon: Icon? = null) : NotificationType(largeIcon)
+
+ private fun getNotification(type: NotificationType): Notification {
+ val notificationBuilder: Notification.Builder =
+ Notification.Builder(mContext, "channelId")
+ .setSmallIcon(R.drawable.ic_person)
+ .setContentTitle(CONTENT_TITLE)
+ .setContentText(CONTENT_TEXT)
+ .setLargeIcon(type.largeIcon)
+
+ val user = Person.Builder().setName("User").build()
+
+ val buildMessagingStyle =
+ Notification.MessagingStyle(user)
+ .setConversationTitle(CONVERSATION_TITLE)
+ .addMessage("Hi", 0, currentUser)
+
+ return when (type) {
+ is NonMessaging ->
+ notificationBuilder
+ .setStyle(Notification.BigTextStyle().bigText("Big Text"))
+ .build()
+ is LegacyMessaging -> {
+ buildMessagingStyle
+ .addMessage("What's up?", 0, firstSender)
+ .addMessage("Not much", 0, currentUser)
+ .addMessage(LAST_MESSAGE, 0, firstSender)
+
+ val notification = notificationBuilder.setStyle(buildMessagingStyle).build()
+
+ assertNull(notification.shortcutId)
+ notification
+ }
+ is LegacyMessagingGroup -> {
+ buildMessagingStyle
+ .addMessage("What's up?", 0, firstSender)
+ .addMessage("Check out my new hover board!", 0, lastSender)
+ .setGroupConversation(true)
+ .addMessage(LAST_MESSAGE, 0, lastSender)
+
+ val notification = notificationBuilder.setStyle(buildMessagingStyle).build()
+
+ assertNull(notification.shortcutId)
+ notification
+ }
+ is OneToOneConversation -> {
+ buildMessagingStyle
+ .addMessage("What's up?", 0, firstSender)
+ .addMessage("Not much", 0, currentUser)
+ .addMessage(LAST_MESSAGE, 0, firstSender)
+ .setShortcutIcon(type.shortcutIcon)
+ notificationBuilder.setShortcutId(SHORTCUT_ID).setStyle(buildMessagingStyle).build()
+ }
+ is GroupConversation -> {
+ buildMessagingStyle
+ .addMessage("What's up?", 0, firstSender)
+ .addMessage("Check out my new hover board!", 0, lastSender)
+ .setGroupConversation(true)
+ .addMessage(LAST_MESSAGE, 0, lastSender)
+ notificationBuilder.setShortcutId(SHORTCUT_ID).setStyle(buildMessagingStyle).build()
+ }
+ }
+ }
+
+ private fun Notification.makeSingleLineViewModel(type: NotificationType): SingleLineViewModel {
+ val builder = Notification.Builder.recoverBuilder(context, this)
+
+ // Validate the recovered builder has the right type of style
+ val expectMessagingStyle =
+ when (type) {
+ is LegacyMessaging,
+ is LegacyMessagingGroup,
+ is OneToOneConversation,
+ is GroupConversation -> true
+ else -> false
+ }
+ if (expectMessagingStyle) {
+ assertIs<Notification.MessagingStyle>(
+ builder.style,
+ "Notification style should be MessagingStyle"
+ )
+ } else {
+ assertIsNot<Notification.MessagingStyle>(
+ builder.style,
+ message = "Notification style should not be MessagingStyle"
+ )
+ }
+
+ // Inflate the SingleLineViewModel
+ // Mock the behavior of NotificationContentInflater.doInBackground
+ val messagingStyle = builder.getMessagingStyle()
+ val isConversation = type is OneToOneConversation || type is GroupConversation
+ return SingleLineViewInflater.inflateSingleLineViewModel(
+ this,
+ if (isConversation) messagingStyle else null,
+ builder,
+ context
+ )
+ }
+
+ private fun Notification.Builder.getMessagingStyle(): Notification.MessagingStyle? {
+ return style as? Notification.MessagingStyle
+ }
+
+ private fun getBitmap(context: Context, resId: Int): Bitmap {
+ val largeIconDimension =
+ context.resources.getDimension(R.dimen.conversation_single_line_avatar_size)
+ val d = context.resources.getDrawable(resId)
+ val b =
+ Bitmap.createBitmap(
+ largeIconDimension.toInt(),
+ largeIconDimension.toInt(),
+ Bitmap.Config.ARGB_8888
+ )
+ val c = Canvas(b)
+ val paint = Paint()
+ c.drawCircle(
+ largeIconDimension / 2,
+ largeIconDimension / 2,
+ largeIconDimension.coerceAtMost(largeIconDimension) / 2,
+ paint
+ )
+ d.setBounds(0, 0, largeIconDimension.toInt(), largeIconDimension.toInt())
+ paint.setXfermode(PorterDuffXfermode(PorterDuff.Mode.SRC_IN))
+ c.saveLayer(0F, 0F, largeIconDimension, largeIconDimension, paint, Canvas.ALL_SAVE_FLAG)
+ d.draw(c)
+ c.restore()
+ return b
+ }
+
+ fun ConversationAvatar.equalsTo(other: ConversationAvatar?): Boolean =
+ when {
+ this === other -> true
+ this is SingleIcon && other is SingleIcon -> equalsTo(other)
+ this is FacePile && other is FacePile -> equalsTo(other)
+ else -> false
+ }
+
+ private fun SingleIcon.equalsTo(other: SingleIcon): Boolean =
+ iconDrawable?.equalsTo(other.iconDrawable) == true
+
+ private fun FacePile.equalsTo(other: FacePile): Boolean =
+ when {
+ bottomBackgroundColor != other.bottomBackgroundColor -> false
+ topIconDrawable?.equalsTo(other.topIconDrawable) != true -> false
+ bottomIconDrawable?.equalsTo(other.bottomIconDrawable) != true -> false
+ else -> true
+ }
+
+ fun Drawable.equalsTo(other: Drawable?): Boolean =
+ when {
+ this === other -> true
+ this.pixelsEqualTo(other) -> true
+ else -> false
+ }
+
+ private fun <T : Drawable> T.pixelsEqualTo(t: T?) =
+ toBitmap().pixelsEqualTo(t?.toBitmap(), false)
+
+ private fun Bitmap.pixelsEqualTo(otherBitmap: Bitmap?, shouldRecycle: Boolean = false) =
+ otherBitmap?.let { other ->
+ if (width == other.width && height == other.height) {
+ val res = toPixels().contentEquals(other.toPixels())
+ if (shouldRecycle) {
+ doRecycle().also { otherBitmap.doRecycle() }
+ }
+ res
+ } else false
+ }
+ ?: kotlin.run { false }
+
+ private fun Bitmap.toPixels() =
+ IntArray(width * height).apply { getPixels(this, 0, width, 0, 0, width, height) }
+
+ fun Bitmap.doRecycle() {
+ if (!isRecycled) recycle()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index 06298b78ae57..32c727c70172 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -38,6 +38,9 @@ import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
+import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
+import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel
import com.android.systemui.keyguard.ui.viewmodel.keyguardRootViewModel
import com.android.systemui.kosmos.testScope
import com.android.systemui.res.R
@@ -45,6 +48,7 @@ import com.android.systemui.shade.data.repository.shadeRepository
import com.android.systemui.shade.mockLargeScreenHeaderHelper
import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor
import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -55,15 +59,22 @@ import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
@SmallTest
@RunWith(AndroidJUnit4::class)
class SharedNotificationContainerViewModelTest : SysuiTestCase() {
+ val aodBurnInViewModel = mock(AodBurnInViewModel::class.java)
+ lateinit var translationYFlow: MutableStateFlow<Float>
val kosmos =
testKosmos().apply {
fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) }
}
+
+ init {
+ kosmos.aodBurnInViewModel = aodBurnInViewModel
+ }
val testScope = kosmos.testScope
val configurationRepository = kosmos.fakeConfigurationRepository
val keyguardRepository = kosmos.fakeKeyguardRepository
@@ -75,11 +86,14 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() {
val sharedNotificationContainerInteractor = kosmos.sharedNotificationContainerInteractor
val largeScreenHeaderHelper = kosmos.mockLargeScreenHeaderHelper
- val underTest = kosmos.sharedNotificationContainerViewModel
+ lateinit var underTest: SharedNotificationContainerViewModel
@Before
fun setUp() {
overrideResource(R.bool.config_use_split_notification_shade, false)
+ translationYFlow = MutableStateFlow(0f)
+ whenever(aodBurnInViewModel.translationY(any())).thenReturn(translationYFlow)
+ underTest = kosmos.sharedNotificationContainerViewModel
}
@Test
@@ -579,9 +593,21 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() {
}
@Test
+ fun translationYUpdatesOnKeyguardForBurnIn() =
+ testScope.runTest {
+ val translationY by collectLastValue(underTest.translationY(BurnInParameters()))
+
+ showLockscreen()
+ assertThat(translationY).isEqualTo(0)
+
+ translationYFlow.value = 150f
+ assertThat(translationY).isEqualTo(150f)
+ }
+
+ @Test
fun translationYUpdatesOnKeyguard() =
testScope.runTest {
- val translationY by collectLastValue(underTest.translationY)
+ val translationY by collectLastValue(underTest.translationY(BurnInParameters()))
configurationRepository.setDimensionPixelSize(
R.dimen.keyguard_translate_distance_on_swipe_up,
@@ -601,7 +627,7 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() {
@Test
fun translationYDoesNotUpdateWhenShadeIsExpanded() =
testScope.runTest {
- val translationY by collectLastValue(underTest.translationY)
+ val translationY by collectLastValue(underTest.translationY(BurnInParameters()))
configurationRepository.setDimensionPixelSize(
R.dimen.keyguard_translate_distance_on_swipe_up,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt
index 35cfa89e56ed..a8f45b0974c4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt
@@ -26,7 +26,7 @@ import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import kotlinx.coroutines.ExperimentalCoroutinesApi
-val Kosmos.aodBurnInViewModel by Fixture {
+var Kosmos.aodBurnInViewModel by Fixture {
AodBurnInViewModel(
burnInInteractor = burnInInteractor,
configurationInteractor = configurationInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
index db4050905200..7c398cd45f90 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
@@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel
import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel
import com.android.systemui.keyguard.ui.viewmodel.glanceableHubToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.lockscreenToGlanceableHubTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.lockscreenToOccludedTransitionViewModel
@@ -40,6 +41,7 @@ val Kosmos.sharedNotificationContainerViewModel by Fixture {
occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel,
lockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel,
glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel,
- lockscreenToGlanceableHubTransitionViewModel = lockscreenToGlanceableHubTransitionViewModel
+ lockscreenToGlanceableHubTransitionViewModel = lockscreenToGlanceableHubTransitionViewModel,
+ aodBurnInViewModel = aodBurnInViewModel,
)
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 45f657d713ad..57c52c2cf408 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -3754,11 +3754,6 @@ final class ActivityManagerShellCommand extends ShellCommand {
}
@Override
- public void onProcessStarted(int pid, int processUid, int packageUid, String packageName,
- String processName) {
- }
-
- @Override
public void onForegroundServicesChanged(int pid, int uid, int serviceTypes) {
}
diff --git a/services/core/java/com/android/server/am/AppFGSTracker.java b/services/core/java/com/android/server/am/AppFGSTracker.java
index fb89b8e4f3b4..1f98aba5bbd7 100644
--- a/services/core/java/com/android/server/am/AppFGSTracker.java
+++ b/services/core/java/com/android/server/am/AppFGSTracker.java
@@ -102,11 +102,6 @@ final class AppFGSTracker extends BaseAppStateDurationsTracker<AppFGSPolicy, Pac
}
@Override
- public void onProcessStarted(int pid, int processUid, int packageUid, String packageName,
- String processName) {
- }
-
- @Override
public void onProcessDied(int pid, int uid) {
}
};
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index f5c34a5da1c1..fa5dbd2543d3 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -2852,7 +2852,6 @@ public final class ProcessList {
? PROC_START_TIMEOUT_WITH_WRAPPER : PROC_START_TIMEOUT);
}
}
- dispatchProcessStarted(app, pid);
checkSlow(app.getStartTime(), "startProcess: done updating pids map");
return true;
}
@@ -4978,22 +4977,6 @@ public final class ProcessList {
}
}
- void dispatchProcessStarted(ProcessRecord app, int pid) {
- int i = mProcessObservers.beginBroadcast();
- while (i > 0) {
- i--;
- final IProcessObserver observer = mProcessObservers.getBroadcastItem(i);
- if (observer != null) {
- try {
- observer.onProcessStarted(pid, app.uid, app.info.uid,
- app.info.packageName, app.processName);
- } catch (RemoteException e) {
- }
- }
- }
- mProcessObservers.finishBroadcast();
- }
-
void dispatchProcessDied(int pid, int uid) {
int i = mProcessObservers.beginBroadcast();
while (i > 0) {
diff --git a/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java b/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java
index cdd147a0ec37..684d6a0fc596 100644
--- a/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java
+++ b/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java
@@ -177,11 +177,6 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
}
@Override
- public void onProcessStarted(int pid, int processUid, int packageUid, String packageName,
- String processName) {
- }
-
- @Override
public void onForegroundServicesChanged(int pid, int uid, int serviceTypes) {
}
};
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
index 77cb08bc02bd..6ec6a123a4e7 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
@@ -204,10 +204,6 @@ public final class DeviceStateManagerService extends SystemService {
}
@Override
- public void onProcessStarted(int pid, int processUid, int packageUid, String packageName,
- String processName) {}
-
- @Override
public void onProcessDied(int pid, int uid) {}
@Override
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index 978f46808e3b..550aed51c8e2 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -214,11 +214,6 @@ public final class MediaProjectionManagerService extends SystemService
}
@Override
- public void onProcessStarted(int pid, int processUid, int packageUid,
- String packageName, String processName) {
- }
-
- @Override
public void onForegroundServicesChanged(int pid, int uid, int serviceTypes) {
MediaProjectionManagerService.this.handleForegroundServicesChanged(pid, uid,
serviceTypes);
diff --git a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
index 1660c3ef952a..8452c0e61a81 100644
--- a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
+++ b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
@@ -152,14 +152,13 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub {
@RequiresPermission(value = android.Manifest.permission.INTERACT_ACROSS_USERS,
conditional = true)
void ensureCallerPreviouslyGeneratedFile(
- Context context, Pair<Integer, String> callingInfo, int userId,
- String bugreportFile, boolean forceUpdateMapping) {
+ Context context, PackageManager packageManager, Pair<Integer, String> callingInfo,
+ int userId, String bugreportFile, boolean forceUpdateMapping) {
synchronized (mLock) {
if (onboardingBugreportV2Enabled()) {
final int uidForUser = Binder.withCleanCallingIdentity(() -> {
try {
- return context.getPackageManager()
- .getPackageUidAsUser(callingInfo.second, userId);
+ return packageManager.getPackageUidAsUser(callingInfo.second, userId);
} catch (PackageManager.NameNotFoundException exception) {
throwInvalidBugreportFileForCallerException(
bugreportFile, callingInfo.second);
@@ -441,8 +440,8 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub {
Slogf.i(TAG, "Retrieving bugreport for %s / %d", callingPackage, callingUid);
try {
mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(
- mContext, new Pair<>(callingUid, callingPackage), userId, bugreportFile,
- /* forceUpdateMapping= */ false);
+ mContext, mContext.getPackageManager(), new Pair<>(callingUid, callingPackage),
+ userId, bugreportFile, /* forceUpdateMapping= */ false);
} catch (IllegalArgumentException e) {
Slog.e(TAG, e.getMessage());
reportError(listener, IDumpstateListener.BUGREPORT_ERROR_NO_BUGREPORT_TO_RETRIEVE);
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index a6598d602d01..c0596bb10823 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -295,8 +295,6 @@ public class UserManagerService extends IUserManager.Stub {
private static final int USER_VERSION = 11;
- private static final int MAX_USER_STRING_LENGTH = 500;
-
private static final long EPOCH_PLUS_30_YEARS = 30L * 365 * 24 * 60 * 60 * 1000L; // ms
static final int WRITE_USER_MSG = 1;
@@ -4692,16 +4690,18 @@ public class UserManagerService extends IUserManager.Stub {
if (userData.persistSeedData) {
if (userData.seedAccountName != null) {
serializer.attribute(null, ATTR_SEED_ACCOUNT_NAME,
- truncateString(userData.seedAccountName));
+ truncateString(userData.seedAccountName,
+ UserManager.MAX_ACCOUNT_STRING_LENGTH));
}
if (userData.seedAccountType != null) {
serializer.attribute(null, ATTR_SEED_ACCOUNT_TYPE,
- truncateString(userData.seedAccountType));
+ truncateString(userData.seedAccountType,
+ UserManager.MAX_ACCOUNT_STRING_LENGTH));
}
}
if (userInfo.name != null) {
serializer.startTag(null, TAG_NAME);
- serializer.text(truncateString(userInfo.name));
+ serializer.text(truncateString(userInfo.name, UserManager.MAX_USER_NAME_LENGTH));
serializer.endTag(null, TAG_NAME);
}
synchronized (mRestrictionsLock) {
@@ -4765,11 +4765,11 @@ public class UserManagerService extends IUserManager.Stub {
serializer.endDocument();
}
- private String truncateString(String original) {
- if (original == null || original.length() <= MAX_USER_STRING_LENGTH) {
+ private String truncateString(String original, int limit) {
+ if (original == null || original.length() <= limit) {
return original;
}
- return original.substring(0, MAX_USER_STRING_LENGTH);
+ return original.substring(0, limit);
}
/*
@@ -5236,7 +5236,7 @@ public class UserManagerService extends IUserManager.Stub {
@UserIdInt int parentId, boolean preCreate, @Nullable String[] disallowedPackages,
@NonNull TimingsTraceAndSlog t, @Nullable Object token)
throws UserManager.CheckedUserOperationException {
- String truncatedName = truncateString(name);
+ String truncatedName = truncateString(name, UserManager.MAX_USER_NAME_LENGTH);
final UserTypeDetails userTypeDetails = mUserTypes.get(userType);
if (userTypeDetails == null) {
throwCheckedUserOperationException(
@@ -6821,9 +6821,14 @@ public class UserManagerService extends IUserManager.Stub {
Slog.e(LOG_TAG, "No such user for settings seed data u=" + userId);
return;
}
- userData.seedAccountName = truncateString(accountName);
- userData.seedAccountType = truncateString(accountType);
- userData.seedAccountOptions = accountOptions;
+ userData.seedAccountName = truncateString(accountName,
+ UserManager.MAX_ACCOUNT_STRING_LENGTH);
+ userData.seedAccountType = truncateString(accountType,
+ UserManager.MAX_ACCOUNT_STRING_LENGTH);
+ if (accountOptions != null && accountOptions.isBundleContentsWithinLengthLimit(
+ UserManager.MAX_ACCOUNT_OPTIONS_LENGTH)) {
+ userData.seedAccountOptions = accountOptions;
+ }
userData.persistSeedData = persist;
}
if (persist) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java
deleted file mode 100644
index fcf761fb6607..000000000000
--- a/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java
+++ /dev/null
@@ -1,275 +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.server.am;
-
-import static android.os.Process.myPid;
-import static android.os.Process.myUid;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-
-import android.app.ActivityManagerInternal;
-import android.app.IApplicationThread;
-import android.app.IProcessObserver;
-import android.app.usage.UsageStatsManagerInternal;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManagerInternal;
-import android.os.Binder;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.IBinder;
-import android.util.Log;
-
-import androidx.test.filters.MediumTest;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.server.DropBoxManagerInternal;
-import com.android.server.LocalServices;
-import com.android.server.am.ActivityManagerService.Injector;
-import com.android.server.appop.AppOpsService;
-import com.android.server.wm.ActivityTaskManagerInternal;
-import com.android.server.wm.ActivityTaskManagerService;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.io.File;
-import java.util.Arrays;
-
-
-/**
- * Tests to verify that process events are dispatched to process observers.
- */
-@MediumTest
-@SuppressWarnings("GuardedBy")
-public class ProcessObserverTest {
- private static final String TAG = "ProcessObserverTest";
-
- private static final String PACKAGE = "com.foo";
-
- @Rule
- public final ApplicationExitInfoTest.ServiceThreadRule
- mServiceThreadRule = new ApplicationExitInfoTest.ServiceThreadRule();
-
- private Context mContext;
- private HandlerThread mHandlerThread;
-
- @Mock
- private AppOpsService mAppOpsService;
- @Mock
- private DropBoxManagerInternal mDropBoxManagerInt;
- @Mock
- private PackageManagerInternal mPackageManagerInt;
- @Mock
- private UsageStatsManagerInternal mUsageStatsManagerInt;
- @Mock
- private ActivityManagerInternal mActivityManagerInt;
- @Mock
- private ActivityTaskManagerInternal mActivityTaskManagerInt;
- @Mock
- private BatteryStatsService mBatteryStatsService;
-
- private ActivityManagerService mRealAms;
- private ActivityManagerService mAms;
-
- private ProcessList mRealProcessList = new ProcessList();
- private ProcessList mProcessList;
-
- final IProcessObserver mProcessObserver = mock(IProcessObserver.Stub.class);
-
- @Before
- public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
-
- mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
-
- mHandlerThread = new HandlerThread(TAG);
- mHandlerThread.start();
-
- LocalServices.removeServiceForTest(DropBoxManagerInternal.class);
- LocalServices.addService(DropBoxManagerInternal.class, mDropBoxManagerInt);
-
- LocalServices.removeServiceForTest(PackageManagerInternal.class);
- LocalServices.addService(PackageManagerInternal.class, mPackageManagerInt);
-
- LocalServices.removeServiceForTest(ActivityManagerInternal.class);
- LocalServices.addService(ActivityManagerInternal.class, mActivityManagerInt);
-
- LocalServices.removeServiceForTest(ActivityTaskManagerInternal.class);
- LocalServices.addService(ActivityTaskManagerInternal.class, mActivityTaskManagerInt);
-
- doReturn(new ComponentName("", "")).when(mPackageManagerInt).getSystemUiServiceComponent();
- doReturn(true).when(mActivityTaskManagerInt).attachApplication(any());
- doNothing().when(mActivityTaskManagerInt).onProcessMapped(anyInt(), any());
-
- mRealAms = new ActivityManagerService(
- new TestInjector(mContext), mServiceThreadRule.getThread());
- mRealAms.mConstants.loadDeviceConfigConstants();
- mRealAms.mActivityTaskManager = new ActivityTaskManagerService(mContext);
- mRealAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper());
- mRealAms.mAtmInternal = mActivityTaskManagerInt;
- mRealAms.mPackageManagerInt = mPackageManagerInt;
- mRealAms.mUsageStatsService = mUsageStatsManagerInt;
- mRealAms.mProcessesReady = true;
- mAms = spy(mRealAms);
- mRealProcessList.mService = mAms;
- mProcessList = spy(mRealProcessList);
-
- doReturn(mProcessObserver).when(mProcessObserver).asBinder();
- mProcessList.registerProcessObserver(mProcessObserver);
-
- doAnswer((invocation) -> {
- Log.v(TAG, "Intercepting isProcStartValidLocked() for "
- + Arrays.toString(invocation.getArguments()));
- return null;
- }).when(mProcessList).isProcStartValidLocked(any(), anyLong());
- }
-
- @After
- public void tearDown() throws Exception {
- mHandlerThread.quit();
- }
-
- private class TestInjector extends Injector {
- TestInjector(Context context) {
- super(context);
- }
-
- @Override
- public AppOpsService getAppOpsService(File recentAccessesFile, File storageFile,
- Handler handler) {
- return mAppOpsService;
- }
-
- @Override
- public Handler getUiHandler(ActivityManagerService service) {
- return mHandlerThread.getThreadHandler();
- }
-
- @Override
- public ProcessList getProcessList(ActivityManagerService service) {
- return mRealProcessList;
- }
-
- @Override
- public BatteryStatsService getBatteryStatsService() {
- return mBatteryStatsService;
- }
- }
-
- private ProcessRecord makeActiveProcessRecord(String packageName)
- throws Exception {
- final ApplicationInfo ai = makeApplicationInfo(packageName);
- return makeActiveProcessRecord(ai);
- }
-
- private ProcessRecord makeActiveProcessRecord(ApplicationInfo ai)
- throws Exception {
- final IApplicationThread thread = mock(IApplicationThread.class);
- final IBinder threadBinder = new Binder();
- doReturn(threadBinder).when(thread).asBinder();
- doAnswer((invocation) -> {
- Log.v(TAG, "Intercepting bindApplication() for "
- + Arrays.toString(invocation.getArguments()));
- if (mRealAms.mConstants.mEnableWaitForFinishAttachApplication) {
- mRealAms.finishAttachApplication(0);
- }
- return null;
- }).when(thread).bindApplication(
- any(), any(),
- any(), any(), anyBoolean(),
- any(), any(),
- any(), any(),
- any(),
- any(), anyInt(),
- anyBoolean(), anyBoolean(),
- anyBoolean(), anyBoolean(), any(),
- any(), any(), any(),
- any(), any(),
- any(), any(),
- any(),
- anyLong(), anyLong());
- final ProcessRecord r = spy(new ProcessRecord(mAms, ai, ai.processName, ai.uid));
- r.setPid(myPid());
- r.setStartUid(myUid());
- r.setHostingRecord(new HostingRecord(HostingRecord.HOSTING_TYPE_BROADCAST));
- r.makeActive(thread, mAms.mProcessStats);
- doNothing().when(r).killLocked(any(), any(), anyInt(), anyInt(), anyBoolean(),
- anyBoolean());
- return r;
- }
-
- static ApplicationInfo makeApplicationInfo(String packageName) {
- final ApplicationInfo ai = new ApplicationInfo();
- ai.packageName = packageName;
- ai.processName = packageName;
- ai.uid = myUid();
- return ai;
- }
-
- /**
- * Verify that a process start event is dispatched to process observers.
- */
- @Test
- public void testNormal() throws Exception {
- ProcessRecord app = startProcess();
- verify(mProcessObserver).onProcessStarted(
- app.getPid(), app.uid, app.info.uid, PACKAGE, PACKAGE);
- }
-
- private ProcessRecord startProcess() throws Exception {
- final ProcessRecord app = makeActiveProcessRecord(PACKAGE);
- final ApplicationInfo appInfo = makeApplicationInfo(PACKAGE);
- mProcessList.handleProcessStartedLocked(app, app.getPid(), /* usingWrapper */ false,
- /* expectedStartSeq */ 0, /* procAttached */ false);
- app.getThread().bindApplication(PACKAGE, appInfo,
- null, null, false,
- null,
- null,
- null, null,
- null,
- null, 0,
- false, false,
- true, false,
- null,
- null, null,
- null,
- null, null, null,
- null, null,
- 0, 0);
- return app;
- }
-
- // TODO: [b/302724778] Remove manual JNI load
- static {
- System.loadLibrary("mockingservicestestjni");
- }
-}
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
index 5bec903e6414..656bc71eebca 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -556,7 +556,7 @@ public final class UserManagerServiceTest {
@Test
public void testCreateUserWithLongName_TruncatesName() {
UserInfo user = mUms.createUserWithThrow(generateLongString(), USER_TYPE_FULL_SECONDARY, 0);
- assertThat(user.name.length()).isEqualTo(500);
+ assertThat(user.name.length()).isEqualTo(UserManager.MAX_USER_NAME_LENGTH);
UserInfo user1 = mUms.createUserWithThrow("Test", USER_TYPE_FULL_SECONDARY, 0);
assertThat(user1.name.length()).isEqualTo(4);
}
diff --git a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
index dc1d2c5e54b6..1c6d36b0a0d2 100644
--- a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
@@ -17,16 +17,19 @@
package com.android.server.os;
import android.app.admin.flags.Flags;
-import static android.app.admin.flags.Flags.onboardingBugreportV2Enabled;
import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
import android.app.role.RoleManager;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.BugreportManager.BugreportCallback;
import android.os.IBinder;
@@ -48,6 +51,8 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
import java.io.FileDescriptor;
import java.util.concurrent.CompletableFuture;
@@ -66,6 +71,9 @@ public class BugreportManagerServiceImplTest {
private BugreportManagerServiceImpl mService;
private BugreportManagerServiceImpl.BugreportFileManager mBugreportFileManager;
+ @Mock
+ private PackageManager mPackageManager;
+
private int mCallingUid = 1234;
private String mCallingPackage = "test.package";
private AtomicFile mMappingFile;
@@ -74,7 +82,8 @@ public class BugreportManagerServiceImplTest {
private String mBugreportFile2 = "bugreport-file2.zip";
@Before
- public void setUp() {
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
mContext = InstrumentationRegistry.getInstrumentation().getContext();
mMappingFile = new AtomicFile(mContext.getFilesDir(), "bugreport-mapping.xml");
ArraySet<String> mAllowlistedPackages = new ArraySet<>();
@@ -83,6 +92,7 @@ public class BugreportManagerServiceImplTest {
new BugreportManagerServiceImpl.Injector(mContext, mAllowlistedPackages,
mMappingFile));
mBugreportFileManager = new BugreportManagerServiceImpl.BugreportFileManager(mMappingFile);
+ when(mPackageManager.getPackageUidAsUser(anyString(), anyInt())).thenReturn(mCallingUid);
}
@After
@@ -115,12 +125,13 @@ public class BugreportManagerServiceImplTest {
assertThrows(IllegalArgumentException.class, () ->
mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(
- mContext, callingInfo, Process.myUserHandle().getIdentifier(),
- "unknown-file.zip", /* forceUpdateMapping= */ true));
+ mContext, mPackageManager, callingInfo,
+ Process.myUserHandle().getIdentifier(), "unknown-file.zip",
+ /* forceUpdateMapping= */ true));
// No exception should be thrown.
mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(
- mContext, callingInfo, mContext.getUserId(), mBugreportFile,
+ mContext, mPackageManager, callingInfo, mContext.getUserId(), mBugreportFile,
/* forceUpdateMapping= */ true);
}
@@ -132,7 +143,7 @@ public class BugreportManagerServiceImplTest {
callingInfo, mBugreportFile, /* keepOnRetrieval= */ true);
mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(
- mContext, callingInfo, mContext.getUserId(), mBugreportFile,
+ mContext, mPackageManager, callingInfo, mContext.getUserId(), mBugreportFile,
/* forceUpdateMapping= */ true);
assertThat(mBugreportFileManager.mBugreportFilesToPersist).containsExactly(mBugreportFile);
@@ -148,10 +159,10 @@ public class BugreportManagerServiceImplTest {
// No exception should be thrown.
mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(
- mContext, callingInfo, mContext.getUserId(), mBugreportFile,
+ mContext, mPackageManager, callingInfo, mContext.getUserId(), mBugreportFile,
/* forceUpdateMapping= */ true);
mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(
- mContext, callingInfo, mContext.getUserId(), mBugreportFile2,
+ mContext, mPackageManager, callingInfo, mContext.getUserId(), mBugreportFile2,
/* forceUpdateMapping= */ true);
}
@@ -160,8 +171,9 @@ public class BugreportManagerServiceImplTest {
Pair<Integer, String> callingInfo = new Pair<>(mCallingUid, mCallingPackage);
assertThrows(IllegalArgumentException.class,
() -> mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(
- mContext, callingInfo, Process.myUserHandle().getIdentifier(),
- "test-file.zip", /* forceUpdateMapping= */ true));
+ mContext, mPackageManager, callingInfo,
+ Process.myUserHandle().getIdentifier(), "test-file.zip",
+ /* forceUpdateMapping= */ true));
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index a743fff5d2ea..06be456be0db 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -19,6 +19,7 @@ package com.android.server.pm;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import static org.testng.Assert.assertEquals;
@@ -33,6 +34,7 @@ import android.content.pm.UserProperties;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
+import android.os.PersistableBundle;
import android.os.UserHandle;
import android.os.UserManager;
import android.platform.test.annotations.Postsubmit;
@@ -1632,6 +1634,106 @@ public final class UserManagerTest {
assertThat(mainUserCount).isEqualTo(1);
}
+ @Test
+ public void testAddUserAccountData_validStringValuesAreSaved_validBundleIsSaved() {
+ assumeManagedUsersSupported();
+
+ String userName = "User";
+ String accountName = "accountName";
+ String accountType = "accountType";
+ String arrayKey = "StringArrayKey";
+ String stringKey = "StringKey";
+ String intKey = "IntKey";
+ String nestedBundleKey = "PersistableBundleKey";
+ String value1 = "Value 1";
+ String value2 = "Value 2";
+ String value3 = "Value 3";
+
+ UserInfo userInfo = mUserManager.createUser(userName,
+ UserManager.USER_TYPE_FULL_SECONDARY, 0);
+
+ PersistableBundle accountOptions = new PersistableBundle();
+ String[] stringArray = {value1, value2};
+ accountOptions.putInt(intKey, 1234);
+ PersistableBundle nested = new PersistableBundle();
+ nested.putString(stringKey, value3);
+ accountOptions.putPersistableBundle(nestedBundleKey, nested);
+ accountOptions.putStringArray(arrayKey, stringArray);
+
+ mUserManager.clearSeedAccountData();
+ mUserManager.setSeedAccountData(mContext.getUserId(), accountName,
+ accountType, accountOptions);
+
+ //assert userName accountName and accountType were saved correctly
+ assertTrue(mUserManager.getUserInfo(userInfo.id).name.equals(userName));
+ assertTrue(mUserManager.getSeedAccountName().equals(accountName));
+ assertTrue(mUserManager.getSeedAccountType().equals(accountType));
+
+ //assert bundle with correct values was added
+ assertThat(mUserManager.getSeedAccountOptions().containsKey(arrayKey)).isTrue();
+ assertThat(mUserManager.getSeedAccountOptions().getPersistableBundle(nestedBundleKey)
+ .getString(stringKey)).isEqualTo(value3);
+ assertThat(mUserManager.getSeedAccountOptions().getStringArray(arrayKey)[0])
+ .isEqualTo(value1);
+
+ mUserManager.removeUser(userInfo.id);
+ }
+
+ @Test
+ public void testAddUserAccountData_invalidStringValuesAreTruncated_invalidBundleIsDropped() {
+ assumeManagedUsersSupported();
+
+ String tooLongString = generateLongString();
+ String userName = "User " + tooLongString;
+ String accountType = "Account Type " + tooLongString;
+ String accountName = "accountName " + tooLongString;
+ String arrayKey = "StringArrayKey";
+ String stringKey = "StringKey";
+ String intKey = "IntKey";
+ String nestedBundleKey = "PersistableBundleKey";
+ String value1 = "Value 1";
+ String value2 = "Value 2";
+
+ UserInfo userInfo = mUserManager.createUser(userName,
+ UserManager.USER_TYPE_FULL_SECONDARY, 0);
+
+ PersistableBundle accountOptions = new PersistableBundle();
+ String[] stringArray = {value1, value2};
+ accountOptions.putInt(intKey, 1234);
+ PersistableBundle nested = new PersistableBundle();
+ nested.putString(stringKey, tooLongString);
+ accountOptions.putPersistableBundle(nestedBundleKey, nested);
+ accountOptions.putStringArray(arrayKey, stringArray);
+ mUserManager.clearSeedAccountData();
+ mUserManager.setSeedAccountData(mContext.getUserId(), accountName,
+ accountType, accountOptions);
+
+ //assert userName was truncated
+ assertTrue(mUserManager.getUserInfo(userInfo.id).name.length()
+ == UserManager.MAX_USER_NAME_LENGTH);
+
+ //assert accountName and accountType got truncated
+ assertTrue(mUserManager.getSeedAccountName().length()
+ == UserManager.MAX_ACCOUNT_STRING_LENGTH);
+ assertTrue(mUserManager.getSeedAccountType().length()
+ == UserManager.MAX_ACCOUNT_STRING_LENGTH);
+
+ //assert bundle with invalid values was dropped
+ assertThat(mUserManager.getSeedAccountOptions() == null).isTrue();
+
+ mUserManager.removeUser(userInfo.id);
+ }
+
+ private String generateLongString() {
+ String partialString = "Test Name Test Name Test Name Test Name Test Name Test Name Test "
+ + "Name Test Name Test Name Test Name "; //String of length 100
+ StringBuilder resultString = new StringBuilder();
+ for (int i = 0; i < 600; i++) {
+ resultString.append(partialString);
+ }
+ return resultString.toString();
+ }
+
private boolean isPackageInstalledForUser(String packageName, int userId) {
try {
return mPackageManager.getPackageInfoAsUser(packageName, 0, userId) != null;