diff options
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; |