From ac9e0f9162969083ffe2bc9b456c5273f70b1a0b Mon Sep 17 00:00:00 2001 From: Anna Zappone Date: Wed, 17 Mar 2021 14:50:18 +0000 Subject: [PATCH] Implement 2x2 layout and text resizing Expand text to best-fit the layout by calculating maxLines for medium and large layout. Bug: 182377480 Test: PeopleSpaceUtils Change-Id: I980ef776dfdf6e3007b8761ec7e1db3ec5fe5b7d --- .../res/layout/people_tile_large_empty.xml | 67 ++ .../layout/people_tile_large_with_content.xml | 109 ++++ ..._tile.xml => people_tile_medium_empty.xml} | 7 +- ...ml => people_tile_medium_with_content.xml} | 72 ++- ...e_small_view.xml => people_tile_small.xml} | 5 +- packages/SystemUI/res/values/dimens.xml | 12 + .../systemui/people/PeopleSpaceUtils.java | 401 +----------- .../systemui/people/PeopleTileViewHelper.java | 553 +++++++++++++++++ .../systemui/people/PeopleSpaceUtilsTest.java | 360 +---------- .../people/PeopleTileViewHelperTest.java | 587 ++++++++++++++++++ 10 files changed, 1386 insertions(+), 787 deletions(-) create mode 100644 packages/SystemUI/res/layout/people_tile_large_empty.xml create mode 100644 packages/SystemUI/res/layout/people_tile_large_with_content.xml rename packages/SystemUI/res/layout/{people_space_large_avatar_tile.xml => people_tile_medium_empty.xml} (94%) rename packages/SystemUI/res/layout/{people_space_small_avatar_tile.xml => people_tile_medium_with_content.xml} (71%) rename packages/SystemUI/res/layout/{people_space_small_view.xml => people_tile_small.xml} (94%) create mode 100644 packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/people/PeopleTileViewHelperTest.java diff --git a/packages/SystemUI/res/layout/people_tile_large_empty.xml b/packages/SystemUI/res/layout/people_tile_large_empty.xml new file mode 100644 index 0000000000000..1e00307bfc23d --- /dev/null +++ b/packages/SystemUI/res/layout/people_tile_large_empty.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/layout/people_tile_large_with_content.xml b/packages/SystemUI/res/layout/people_tile_large_with_content.xml new file mode 100644 index 0000000000000..f2341b55488f1 --- /dev/null +++ b/packages/SystemUI/res/layout/people_tile_large_with_content.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/layout/people_space_large_avatar_tile.xml b/packages/SystemUI/res/layout/people_tile_medium_empty.xml similarity index 94% rename from packages/SystemUI/res/layout/people_space_large_avatar_tile.xml rename to packages/SystemUI/res/layout/people_tile_medium_empty.xml index b1a37bf71beb5..c35787efec5d0 100644 --- a/packages/SystemUI/res/layout/people_space_large_avatar_tile.xml +++ b/packages/SystemUI/res/layout/people_tile_medium_empty.xml @@ -35,16 +35,17 @@ android:layout_height="match_parent"> + android:layout_width="64dp" + android:layout_height="64dp"/> diff --git a/packages/SystemUI/res/layout/people_space_small_avatar_tile.xml b/packages/SystemUI/res/layout/people_tile_medium_with_content.xml similarity index 71% rename from packages/SystemUI/res/layout/people_space_small_avatar_tile.xml rename to packages/SystemUI/res/layout/people_tile_medium_with_content.xml index 6a606e10e3fb3..e4e4cd8956a72 100644 --- a/packages/SystemUI/res/layout/people_space_small_avatar_tile.xml +++ b/packages/SystemUI/res/layout/people_tile_medium_with_content.xml @@ -27,28 +27,37 @@ android:layout_gravity="center" android:padding="8dp" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="match_parent"> + - + android:layout_height="0dp"> + + + + android:background="@drawable/circle_green_10dp" /> + + - + android:singleLine="true" + android:ellipsize="end" /> + + + - - + android:maxLines="2" + android:singleLine="false" + android:ellipsize="end" /> + @@ -101,16 +108,17 @@ android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" android:textColor="?android:attr/textColorPrimary" android:textSize="14sp" - android:maxLines="1" + android:singleLine="true" android:ellipsize="end" android:layout_width="wrap_content" - android:layout_height="wrap_content"/> + android:layout_height="wrap_content" /> + + android:layout_height="18dp" /> diff --git a/packages/SystemUI/res/layout/people_space_small_view.xml b/packages/SystemUI/res/layout/people_tile_small.xml similarity index 94% rename from packages/SystemUI/res/layout/people_space_small_view.xml rename to packages/SystemUI/res/layout/people_tile_small.xml index 7b1abaed28ca9..914ee3c089d41 100644 --- a/packages/SystemUI/res/layout/people_space_small_view.xml +++ b/packages/SystemUI/res/layout/people_tile_small.xml @@ -25,7 +25,8 @@ android:background="@drawable/people_space_tile_view_card" android:orientation="vertical" android:paddingHorizontal="4dp" - android:paddingVertical="8dp"> + android:paddingTop="6dp" + android:paddingBottom="8dp"> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index b050945068f22..31da4f0afdf7e 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1355,6 +1355,18 @@ 28dp 20dp 6dp + 146dp + 138dp + 182dp + 146dp + 92dp + 52dp + 64dp + 108dp + 14sp + 24sp + 12sp + 14sp 5dp diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceUtils.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceUtils.java index f9c2a2aed7516..93ce5a83f6840 100644 --- a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceUtils.java +++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceUtils.java @@ -18,42 +18,23 @@ package com.android.systemui.people; import static android.app.Notification.CATEGORY_MISSED_CALL; import static android.app.Notification.EXTRA_MESSAGES; -import static android.app.people.ConversationStatus.ACTIVITY_ANNIVERSARY; -import static android.app.people.ConversationStatus.ACTIVITY_AUDIO; -import static android.app.people.ConversationStatus.ACTIVITY_BIRTHDAY; -import static android.app.people.ConversationStatus.ACTIVITY_GAME; -import static android.app.people.ConversationStatus.ACTIVITY_LOCATION; -import static android.app.people.ConversationStatus.ACTIVITY_NEW_STORY; -import static android.app.people.ConversationStatus.ACTIVITY_UPCOMING_BIRTHDAY; -import static android.app.people.ConversationStatus.ACTIVITY_VIDEO; -import static android.app.people.ConversationStatus.AVAILABILITY_AVAILABLE; -import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT; -import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH; -import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT; -import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH; -import android.annotation.Nullable; import android.app.INotificationManager; import android.app.Notification; -import android.app.PendingIntent; import android.app.people.ConversationChannel; -import android.app.people.ConversationStatus; import android.app.people.IPeopleManager; import android.app.people.PeopleSpaceTile; import android.appwidget.AppWidgetManager; import android.content.Context; -import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.LauncherApps; import android.content.pm.ShortcutInfo; -import android.content.res.Configuration; import android.database.Cursor; import android.database.SQLException; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; import android.icu.text.MeasureFormat; import android.icu.util.Measure; import android.icu.util.MeasureUnit; @@ -66,10 +47,7 @@ import android.provider.ContactsContract; import android.service.notification.ConversationChannelWrapper; import android.service.notification.StatusBarNotification; import android.text.TextUtils; -import android.util.IconDrawableFactory; import android.util.Log; -import android.util.TypedValue; -import android.view.View; import android.widget.RemoteViews; import androidx.preference.PreferenceManager; @@ -81,8 +59,6 @@ import com.android.internal.util.ArrayUtils; import com.android.settingslib.utils.ThreadUtils; import com.android.systemui.R; import com.android.systemui.people.widget.AppWidgetOptionsHelper; -import com.android.systemui.people.widget.LaunchConversationActivity; -import com.android.systemui.people.widget.PeopleSpaceWidgetProvider; import com.android.systemui.people.widget.PeopleTileKey; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -99,10 +75,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -123,18 +96,6 @@ public class PeopleSpaceUtils { public static final PeopleTileKey EMPTY_KEY = new PeopleTileKey(EMPTY_STRING, INVALID_USER_ID, EMPTY_STRING); - public static final int SMALL_LAYOUT = 0; - public static final int MEDIUM_LAYOUT = 1; - @VisibleForTesting - static final int REQUIRED_WIDTH_FOR_MEDIUM = 146; - private static final int AVATAR_SIZE_FOR_MEDIUM = 56; - private static final int DEFAULT_WIDTH = 146; - private static final int DEFAULT_HEIGHT = 92; - - private static final Pattern DOUBLE_EXCLAMATION_PATTERN = Pattern.compile("[!][!]+"); - private static final Pattern DOUBLE_QUESTION_PATTERN = Pattern.compile("[?][?]+"); - private static final Pattern ANY_DOUBLE_MARK_PATTERN = Pattern.compile("[!?][!?]+"); - private static final Pattern MIXED_MARK_PATTERN = Pattern.compile("![?].*|.*[?]!"); /** Represents whether {@link StatusBarNotification} was posted or removed. */ public enum NotificationAction { @@ -336,363 +297,6 @@ public class PeopleSpaceUtils { .build(); } - /** Creates a {@link RemoteViews} for {@code tile}. */ - public static RemoteViews createRemoteViews(Context context, - PeopleSpaceTile tile, int appWidgetId, Bundle options) { - int layoutSize = getLayoutSize(context, options); - RemoteViews viewsForTile = getViewForTile(context, tile, layoutSize); - int maxAvatarSize = getMaxAvatarSize(context, options, layoutSize); - RemoteViews views = setCommonRemoteViewsFields(context, viewsForTile, tile, maxAvatarSize); - return setLaunchIntents(context, views, tile, appWidgetId); - } - - /** - * The prioritization for the {@code tile} content is missed calls, followed by notification - * content, then birthdays, then the most recent status, and finally last interaction. - */ - private static RemoteViews getViewForTile(Context context, PeopleSpaceTile tile, - int layoutSize) { - if (Objects.equals(tile.getNotificationCategory(), CATEGORY_MISSED_CALL)) { - if (DEBUG) Log.d(TAG, "Create missed call view"); - return createMissedCallRemoteViews(context, tile, layoutSize); - } - - if (tile.getNotificationKey() != null) { - if (DEBUG) Log.d(TAG, "Create notification view"); - return createNotificationRemoteViews(context, tile, layoutSize); - } - - // TODO: Add sorting when we expose timestamp of statuses. - List statusesForEntireView = - tile.getStatuses() == null ? Arrays.asList() : tile.getStatuses().stream().filter( - c -> isStatusValidForEntireStatusView(c)).collect(Collectors.toList()); - ConversationStatus birthdayStatus = getBirthdayStatus(tile, statusesForEntireView); - if (birthdayStatus != null) { - if (DEBUG) Log.d(TAG, "Create birthday view"); - return createStatusRemoteViews(context, birthdayStatus, layoutSize); - } - - if (!statusesForEntireView.isEmpty()) { - if (DEBUG) { - Log.d(TAG, - "Create status view for: " + statusesForEntireView.get(0).getActivity()); - } - return createStatusRemoteViews(context, statusesForEntireView.get(0), layoutSize); - } - - return createLastInteractionRemoteViews(context, tile, layoutSize); - } - - /** Calculates the best layout relative to the size in {@code options}. */ - private static int getLayoutSize(Context context, Bundle options) { - int display = context.getResources().getConfiguration().orientation; - int width = display == Configuration.ORIENTATION_PORTRAIT - ? options.getInt(OPTION_APPWIDGET_MIN_WIDTH, DEFAULT_WIDTH) : options.getInt( - OPTION_APPWIDGET_MAX_WIDTH, DEFAULT_WIDTH); - int height = display == Configuration.ORIENTATION_PORTRAIT ? options.getInt( - OPTION_APPWIDGET_MAX_HEIGHT, DEFAULT_HEIGHT) - : options.getInt(OPTION_APPWIDGET_MIN_HEIGHT, DEFAULT_HEIGHT); - // Small layout used below a certain minimum width with any height. - if (width < REQUIRED_WIDTH_FOR_MEDIUM) { - if (DEBUG) Log.d(TAG, "Small view for width: " + width + " height: " + height); - return SMALL_LAYOUT; - } - if (DEBUG) Log.d(TAG, "Medium view for width: " + width + " height: " + height); - return MEDIUM_LAYOUT; - } - - /** Returns the max avatar size for {@code layoutSize} under the current {@code options}. */ - private static int getMaxAvatarSize(Context context, Bundle options, int layoutSize) { - int avatarHeightSpace = AVATAR_SIZE_FOR_MEDIUM; - int avatarWidthSpace = AVATAR_SIZE_FOR_MEDIUM; - - if (layoutSize == SMALL_LAYOUT) { - int display = context.getResources().getConfiguration().orientation; - int width = display == Configuration.ORIENTATION_PORTRAIT - ? options.getInt(OPTION_APPWIDGET_MIN_WIDTH, DEFAULT_WIDTH) : options.getInt( - OPTION_APPWIDGET_MAX_WIDTH, DEFAULT_WIDTH); - int height = display == Configuration.ORIENTATION_PORTRAIT ? options.getInt( - OPTION_APPWIDGET_MAX_HEIGHT, DEFAULT_HEIGHT) - : options.getInt(OPTION_APPWIDGET_MIN_HEIGHT, DEFAULT_HEIGHT); - avatarHeightSpace = height - (8 + 4 + 18 + 8); - avatarWidthSpace = width - (4 + 4); - } - if (DEBUG) Log.d(TAG, "Height: " + avatarHeightSpace + " width: " + avatarWidthSpace); - return Math.min(avatarHeightSpace, avatarWidthSpace); - } - - @Nullable - private static ConversationStatus getBirthdayStatus(PeopleSpaceTile tile, - List statuses) { - Optional birthdayStatus = statuses.stream().filter( - c -> c.getActivity() == ACTIVITY_BIRTHDAY).findFirst(); - if (birthdayStatus.isPresent()) { - return birthdayStatus.get(); - } - if (!TextUtils.isEmpty(tile.getBirthdayText())) { - return new ConversationStatus.Builder(tile.getId(), ACTIVITY_BIRTHDAY).build(); - } - - return null; - } - - /** - * Returns whether a {@code status} should have its own entire templated view. - * - *

A status may still be shown on the view (for example, as a new story ring) even if it's - * not valid to compose an entire view. - */ - private static boolean isStatusValidForEntireStatusView(ConversationStatus status) { - switch (status.getActivity()) { - // Birthday & Anniversary don't require text provided or icon provided. - case ACTIVITY_BIRTHDAY: - case ACTIVITY_ANNIVERSARY: - return true; - default: - // For future birthday, location, new story, video, music, game, and other, the - // app must provide either text or an icon. - return !TextUtils.isEmpty(status.getDescription()) - || status.getIcon() != null; - } - } - - private static RemoteViews createStatusRemoteViews(Context context, ConversationStatus status, - int layoutSize) { - int layout = layoutSize == SMALL_LAYOUT ? R.layout.people_space_small_view - : R.layout.people_space_small_avatar_tile; - RemoteViews views = new RemoteViews(context.getPackageName(), layout); - CharSequence statusText = status.getDescription(); - if (TextUtils.isEmpty(statusText)) { - statusText = getStatusTextByType(context, status.getActivity()); - } - views.setViewVisibility(R.id.subtext, View.GONE); - views.setViewVisibility(R.id.text_content, View.VISIBLE); - TypedValue typedValue = new TypedValue(); - context.getTheme().resolveAttribute(android.R.attr.textColorSecondary, typedValue, true); - int secondaryTextColor = context.getColor(typedValue.resourceId); - views.setInt(R.id.text_content, "setTextColor", secondaryTextColor); - views.setTextViewText(R.id.text_content, statusText); - Icon statusIcon = status.getIcon(); - if (statusIcon != null) { - views.setImageViewIcon(R.id.image, statusIcon); - views.setBoolean(R.id.content_background, "setClipToOutline", true); - } else { - views.setViewVisibility(R.id.content_background, View.GONE); - } - // TODO: Set status pre-defined icons - views.setImageViewResource(R.id.predefined_icon, R.drawable.ic_person); - ensurePredefinedIconVisibleOnSmallView(views, layoutSize); - return views; - } - - private static String getStatusTextByType(Context context, int activity) { - switch (activity) { - case ACTIVITY_BIRTHDAY: - return context.getString(R.string.birthday_status); - case ACTIVITY_UPCOMING_BIRTHDAY: - return context.getString(R.string.upcoming_birthday_status); - case ACTIVITY_ANNIVERSARY: - return context.getString(R.string.anniversary_status); - case ACTIVITY_LOCATION: - return context.getString(R.string.location_status); - case ACTIVITY_NEW_STORY: - return context.getString(R.string.new_story_status); - case ACTIVITY_VIDEO: - return context.getString(R.string.video_status); - case ACTIVITY_AUDIO: - return context.getString(R.string.audio_status); - case ACTIVITY_GAME: - return context.getString(R.string.game_status); - default: - return EMPTY_STRING; - } - } - - private static RemoteViews setCommonRemoteViewsFields(Context context, RemoteViews views, - PeopleSpaceTile tile, int maxAvatarSize) { - try { - boolean isAvailable = - tile.getStatuses() != null && tile.getStatuses().stream().anyMatch( - c -> c.getAvailability() == AVAILABILITY_AVAILABLE); - if (isAvailable) { - views.setViewVisibility(R.id.availability, View.VISIBLE); - } else { - views.setViewVisibility(R.id.availability, View.GONE); - } - boolean hasNewStory = - tile.getStatuses() != null && tile.getStatuses().stream().anyMatch( - c -> c.getActivity() == ACTIVITY_NEW_STORY); - views.setTextViewText(R.id.name, tile.getUserName().toString()); - views.setBoolean(R.id.content_background, "setClipToOutline", true); - Icon icon = tile.getUserIcon(); - PeopleStoryIconFactory storyIcon = new PeopleStoryIconFactory(context, - context.getPackageManager(), - IconDrawableFactory.newInstance(context, false), - maxAvatarSize); - Drawable drawable = icon.loadDrawable(context); - Drawable personDrawable = storyIcon.getPeopleTileDrawable(drawable, - tile.getPackageName(), getUserId(tile), tile.isImportantConversation(), - hasNewStory); - Bitmap bitmap = convertDrawableToBitmap(personDrawable); - views.setImageViewBitmap(R.id.person_icon, bitmap); - - return views; - } catch (Exception e) { - Log.e(TAG, "Failed to set common fields: " + e); - } - return views; - } - - private static RemoteViews setLaunchIntents(Context context, RemoteViews views, - PeopleSpaceTile tile, int appWidgetId) { - try { - Intent activityIntent = new Intent(context, LaunchConversationActivity.class); - activityIntent.addFlags( - Intent.FLAG_ACTIVITY_NEW_TASK - | Intent.FLAG_ACTIVITY_CLEAR_TASK - | Intent.FLAG_ACTIVITY_NO_HISTORY - | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - activityIntent.putExtra(PeopleSpaceWidgetProvider.EXTRA_TILE_ID, tile.getId()); - activityIntent.putExtra( - PeopleSpaceWidgetProvider.EXTRA_PACKAGE_NAME, tile.getPackageName()); - activityIntent.putExtra(PeopleSpaceWidgetProvider.EXTRA_USER_HANDLE, - tile.getUserHandle()); - activityIntent.putExtra( - PeopleSpaceWidgetProvider.EXTRA_NOTIFICATION_KEY, tile.getNotificationKey()); - views.setOnClickPendingIntent(R.id.item, PendingIntent.getActivity( - context, - appWidgetId, - activityIntent, - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE)); - return views; - } catch (Exception e) { - Log.e(TAG, "Failed to add launch intents: " + e); - } - - return views; - } - - private static RemoteViews createMissedCallRemoteViews(Context context, - PeopleSpaceTile tile, int layoutSize) { - int layout = layoutSize == SMALL_LAYOUT ? R.layout.people_space_small_view - : R.layout.people_space_small_avatar_tile; - RemoteViews views = new RemoteViews(context.getPackageName(), layout); - views.setViewVisibility(R.id.subtext, View.GONE); - views.setViewVisibility(R.id.text_content, View.VISIBLE); - views.setTextViewText(R.id.text_content, tile.getNotificationContent()); - views.setImageViewResource(R.id.predefined_icon, R.drawable.ic_phone_missed); - ensurePredefinedIconVisibleOnSmallView(views, layoutSize); - views.setBoolean(R.id.content_background, "setClipToOutline", true); - return views; - } - - private static void ensurePredefinedIconVisibleOnSmallView(RemoteViews views, int layoutSize) { - if (layoutSize == SMALL_LAYOUT) { - views.setViewVisibility(R.id.name, View.GONE); - views.setViewVisibility(R.id.predefined_icon, View.VISIBLE); - } - } - - private static RemoteViews createNotificationRemoteViews(Context context, - PeopleSpaceTile tile, int layoutSize) { - int layout = layoutSize == SMALL_LAYOUT ? R.layout.people_space_small_view - : R.layout.people_space_small_avatar_tile; - RemoteViews views = new RemoteViews(context.getPackageName(), layout); - if (layoutSize != MEDIUM_LAYOUT) { - views.setViewVisibility(R.id.name, View.GONE); - views.setViewVisibility(R.id.predefined_icon, View.VISIBLE); - } - Uri image = tile.getNotificationDataUri(); - ensurePredefinedIconVisibleOnSmallView(views, layoutSize); - if (image != null) { - // TODO: Use NotificationInlineImageCache - views.setImageViewUri(R.id.image, image); - views.setViewVisibility(R.id.content_background, View.VISIBLE); - views.setBoolean(R.id.content_background, "setClipToOutline", true); - views.setViewVisibility(R.id.text_content, View.GONE); - views.setImageViewResource(R.id.predefined_icon, R.drawable.ic_photo_camera); - } else { - CharSequence content = tile.getNotificationContent(); - views = setPunctuationRemoteViewsFields(views, content); - views.setTextViewText(R.id.text_content, tile.getNotificationContent()); - // TODO: Measure max lines from height. - views.setInt(R.id.text_content, "setMaxLines", 2); - TypedValue typedValue = new TypedValue(); - context.getTheme().resolveAttribute(android.R.attr.textColorPrimary, typedValue, true); - int primaryTextColor = context.getColor(typedValue.resourceId); - views.setInt(R.id.text_content, "setTextColor", primaryTextColor); - views.setViewVisibility(R.id.text_content, View.VISIBLE); - views.setViewVisibility(R.id.content_background, View.GONE); - views.setImageViewResource(R.id.predefined_icon, R.drawable.ic_message); - } - // TODO: Set subtext as Group Sender name once storing the name in PeopleSpaceTile. - views.setViewVisibility(R.id.subtext, View.GONE); - return views; - } - - private static RemoteViews createLastInteractionRemoteViews(Context context, - PeopleSpaceTile tile, int layoutSize) { - int layout = layoutSize == SMALL_LAYOUT ? R.layout.people_space_small_view - : R.layout.people_space_large_avatar_tile; - RemoteViews views = new RemoteViews(context.getPackageName(), layout); - if (layoutSize == SMALL_LAYOUT) { - views.setViewVisibility(R.id.name, View.VISIBLE); - views.setViewVisibility(R.id.predefined_icon, View.GONE); - } - String status = PeopleSpaceUtils.getLastInteractionString( - context, tile.getLastInteractionTimestamp()); - views.setTextViewText(R.id.last_interaction, status); - return views; - } - - private static RemoteViews setPunctuationRemoteViewsFields( - RemoteViews views, CharSequence content) { - String punctuation = getBackgroundTextFromMessage(content.toString()); - int visibility = View.GONE; - if (punctuation != null) { - visibility = View.VISIBLE; - } - views.setTextViewText(R.id.punctuation1, punctuation); - views.setTextViewText(R.id.punctuation2, punctuation); - views.setTextViewText(R.id.punctuation3, punctuation); - views.setTextViewText(R.id.punctuation4, punctuation); - views.setTextViewText(R.id.punctuation5, punctuation); - views.setTextViewText(R.id.punctuation6, punctuation); - - views.setViewVisibility(R.id.punctuation1, visibility); - views.setViewVisibility(R.id.punctuation2, visibility); - views.setViewVisibility(R.id.punctuation3, visibility); - views.setViewVisibility(R.id.punctuation4, visibility); - views.setViewVisibility(R.id.punctuation5, visibility); - views.setViewVisibility(R.id.punctuation6, visibility); - - return views; - } - - /** Gets character for tile background decoration based on notification content. */ - @VisibleForTesting - static String getBackgroundTextFromMessage(String message) { - if (!ANY_DOUBLE_MARK_PATTERN.matcher(message).find()) { - return null; - } - if (MIXED_MARK_PATTERN.matcher(message).find()) { - return "!?"; - } - Matcher doubleQuestionMatcher = DOUBLE_QUESTION_PATTERN.matcher(message); - if (!doubleQuestionMatcher.find()) { - return "!"; - } - Matcher doubleExclamationMatcher = DOUBLE_EXCLAMATION_PATTERN.matcher(message); - if (!doubleExclamationMatcher.find()) { - return "?"; - } - // If we have both "!!" and "??", return the one that comes first. - if (doubleQuestionMatcher.start() < doubleExclamationMatcher.start()) { - return "?"; - } - return "!"; - } - /** Gets the most recent {@link Notification.MessagingStyle.Message} from the notification. */ @VisibleForTesting public static Notification.MessagingStyle.Message getLastMessagingStyleMessage( @@ -917,7 +521,8 @@ public class PeopleSpaceUtils { public static void updateAppWidgetViews(AppWidgetManager appWidgetManager, Context context, int appWidgetId, PeopleSpaceTile tile, Bundle options) { if (DEBUG) Log.d(TAG, "Widget: " + appWidgetId + ", " + tile.getUserName()); - RemoteViews views = createRemoteViews(context, tile, appWidgetId, options); + RemoteViews views = new PeopleTileViewHelper(context, tile, appWidgetId, + options).getViews(); // Tell the AppWidgetManager to perform an update on the current app widget. appWidgetManager.updateAppWidget(appWidgetId, views); @@ -1005,7 +610,7 @@ public class PeopleSpaceUtils { if (DEBUG) Log.i(TAG, "Returning tile preview for shortcutId: " + shortcutId); Bundle bundle = new Bundle(); - return PeopleSpaceUtils.createRemoteViews(context, augmentedTile, 0, bundle); + return new PeopleTileViewHelper(context, augmentedTile, 0, bundle).getViews(); } /** Returns the userId associated with a {@link PeopleSpaceTile} */ diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java b/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java new file mode 100644 index 0000000000000..ae81ab04ec101 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java @@ -0,0 +1,553 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.people; + +import static android.app.Notification.CATEGORY_MISSED_CALL; +import static android.app.people.ConversationStatus.ACTIVITY_ANNIVERSARY; +import static android.app.people.ConversationStatus.ACTIVITY_AUDIO; +import static android.app.people.ConversationStatus.ACTIVITY_BIRTHDAY; +import static android.app.people.ConversationStatus.ACTIVITY_GAME; +import static android.app.people.ConversationStatus.ACTIVITY_LOCATION; +import static android.app.people.ConversationStatus.ACTIVITY_NEW_STORY; +import static android.app.people.ConversationStatus.ACTIVITY_UPCOMING_BIRTHDAY; +import static android.app.people.ConversationStatus.ACTIVITY_VIDEO; +import static android.app.people.ConversationStatus.AVAILABILITY_AVAILABLE; +import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT; +import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH; +import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT; +import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH; + +import static com.android.systemui.people.PeopleSpaceUtils.convertDrawableToBitmap; +import static com.android.systemui.people.PeopleSpaceUtils.getUserId; + +import android.annotation.Nullable; +import android.app.PendingIntent; +import android.app.people.ConversationStatus; +import android.app.people.PeopleSpaceTile; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.net.Uri; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.IconDrawableFactory; +import android.util.Log; +import android.util.TypedValue; +import android.view.View; +import android.widget.RemoteViews; +import android.widget.TextView; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.R; +import com.android.systemui.people.widget.LaunchConversationActivity; +import com.android.systemui.people.widget.PeopleSpaceWidgetProvider; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +class PeopleTileViewHelper { + /** Turns on debugging information about People Space. */ + public static final boolean DEBUG = true; + private static final String TAG = "PeopleTileView"; + + public static final int LAYOUT_SMALL = 0; + public static final int LAYOUT_MEDIUM = 1; + public static final int LAYOUT_LARGE = 2; + + private static final int MIN_CONTENT_MAX_LINES = 2; + + private static final int FIXED_HEIGHT_DIMENS_FOR_LARGE_CONTENT = 14 + 12 + 4 + 16; + private static final int FIXED_HEIGHT_DIMENS_FOR_MEDIUM_CONTENT = 8 + 4 + 4 + 8; + private static final int FIXED_HEIGHT_DIMENS_FOR_SMALL = 6 + 4 + 8; + private static final int FIXED_WIDTH_DIMENS_FOR_SMALL = 4 + 4; + + private static final Pattern DOUBLE_EXCLAMATION_PATTERN = Pattern.compile("[!][!]+"); + private static final Pattern DOUBLE_QUESTION_PATTERN = Pattern.compile("[?][?]+"); + private static final Pattern ANY_DOUBLE_MARK_PATTERN = Pattern.compile("[!?][!?]+"); + private static final Pattern MIXED_MARK_PATTERN = Pattern.compile("![?].*|.*[?]!"); + + public static final String EMPTY_STRING = ""; + + private Context mContext; + private PeopleSpaceTile mTile; + private float mDensity; + private int mAppWidgetId; + private int mWidth; + private int mHeight; + private int mLayoutSize; + + PeopleTileViewHelper(Context context, PeopleSpaceTile tile, + int appWidgetId, Bundle options) { + mContext = context; + mTile = tile; + mAppWidgetId = appWidgetId; + mDensity = mContext.getResources().getDisplayMetrics().density; + int display = mContext.getResources().getConfiguration().orientation; + mWidth = display == Configuration.ORIENTATION_PORTRAIT + ? options.getInt(OPTION_APPWIDGET_MIN_WIDTH, + getSizeInDp(R.dimen.default_width)) : options.getInt( + OPTION_APPWIDGET_MAX_WIDTH, + getSizeInDp(R.dimen.default_width)); + mHeight = display == Configuration.ORIENTATION_PORTRAIT ? options.getInt( + OPTION_APPWIDGET_MAX_HEIGHT, + getSizeInDp(R.dimen.default_height)) + : options.getInt(OPTION_APPWIDGET_MIN_HEIGHT, + getSizeInDp(R.dimen.default_height)); + mLayoutSize = getLayoutSize(); + } + + public RemoteViews getViews() { + RemoteViews viewsForTile = getViewForTile(); + int maxAvatarSize = getMaxAvatarSize(viewsForTile); + RemoteViews views = setCommonRemoteViewsFields(viewsForTile, maxAvatarSize); + return setLaunchIntents(views); + } + + /** + * The prioritization for the {@code mTile} content is missed calls, followed by notification + * content, then birthdays, then the most recent status, and finally last interaction. + */ + private RemoteViews getViewForTile() { + if (Objects.equals(mTile.getNotificationCategory(), CATEGORY_MISSED_CALL)) { + if (DEBUG) Log.d(TAG, "Create missed call view"); + return createMissedCallRemoteViews(); + } + + if (mTile.getNotificationKey() != null) { + if (DEBUG) Log.d(TAG, "Create notification view"); + return createNotificationRemoteViews(); + } + + // TODO: Add sorting when we expose timestamp of statuses. + List statusesForEntireView = + mTile.getStatuses() == null ? Arrays.asList() : mTile.getStatuses().stream().filter( + c -> isStatusValidForEntireStatusView(c)).collect(Collectors.toList()); + ConversationStatus birthdayStatus = getBirthdayStatus(statusesForEntireView); + if (birthdayStatus != null) { + if (DEBUG) Log.d(TAG, "Create birthday view"); + return createStatusRemoteViews(birthdayStatus); + } + + if (!statusesForEntireView.isEmpty()) { + if (DEBUG) { + Log.d(TAG, + "Create status view for: " + statusesForEntireView.get(0).getActivity()); + } + return createStatusRemoteViews(statusesForEntireView.get(0)); + } + + return createLastInteractionRemoteViews(); + } + + private void setMaxLines(RemoteViews views) { + int textSize = mLayoutSize == LAYOUT_LARGE ? getSizeInDp( + R.dimen.content_text_size_for_medium) + : getSizeInDp(R.dimen.content_text_size_for_medium); + int lineHeight = getLineHeight(textSize); + int notificationContentHeight = getContentHeightForLayout(lineHeight); + int maxAdaptiveLines = Math.floorDiv(notificationContentHeight, lineHeight); + int maxLines = Math.max(MIN_CONTENT_MAX_LINES, maxAdaptiveLines); + views.setInt(R.id.text_content, "setMaxLines", maxLines); + } + + private int getLineHeight(int textSize) { + try { + TextView text = new TextView(mContext); + text.setTextSize(textSize); + int lineHeight = (int) (text.getLineHeight() / mDensity); + return lineHeight; + } catch (Exception e) { + Log.e(TAG, "Could not create text view: " + e); + return getSizeInDp( + R.dimen.content_text_size_for_medium); + } + } + + private int getSizeInDp(int dimenResourceId) { + return (int) (mContext.getResources().getDimension(dimenResourceId) / mDensity); + } + + private int getContentHeightForLayout(int lineHeight) { + switch (mLayoutSize) { + case LAYOUT_MEDIUM: + return mHeight - (lineHeight + FIXED_HEIGHT_DIMENS_FOR_MEDIUM_CONTENT); + case LAYOUT_LARGE: + return mHeight - (getSizeInDp( + R.dimen.max_people_avatar_size_for_large_content) + lineHeight + + FIXED_HEIGHT_DIMENS_FOR_LARGE_CONTENT); + default: + return -1; + } + } + + /** Calculates the best layout relative to the size in {@code options}. */ + private int getLayoutSize() { + if (mHeight >= getSizeInDp(R.dimen.required_width_for_large) + && mWidth >= getSizeInDp(R.dimen.required_width_for_large)) { + if (DEBUG) Log.d(TAG, "Large view for mWidth: " + mWidth + " mHeight: " + mHeight); + return LAYOUT_LARGE; + } + // Small layout used below a certain minimum mWidth with any mHeight. + if (mWidth >= getSizeInDp(R.dimen.required_width_for_medium)) { + if (DEBUG) Log.d(TAG, "Medium view for mWidth: " + mWidth + " mHeight: " + mHeight); + return LAYOUT_MEDIUM; + } + // Small layout can always handle our minimum mWidth and mHeight for our widget. + if (DEBUG) Log.d(TAG, "Small view for mWidth: " + mWidth + " mHeight: " + mHeight); + return LAYOUT_SMALL; + } + + /** Returns the max avatar size for {@code views} under the current {@code options}. */ + private int getMaxAvatarSize(RemoteViews views) { + int layoutId = views.getLayoutId(); + int avatarSize = getSizeInDp(R.dimen.avatar_size_for_medium); + if (layoutId == R.layout.people_tile_medium_empty) { + return getSizeInDp( + R.dimen.max_people_avatar_size_for_large_content); + } + if (layoutId == R.layout.people_tile_medium_with_content) { + return getSizeInDp(R.dimen.avatar_size_for_medium); + } + + // Calculate adaptive avatar size for remaining layouts. + if (layoutId == R.layout.people_tile_small) { + int avatarHeightSpace = mHeight - (FIXED_HEIGHT_DIMENS_FOR_SMALL + Math.max(18, + getLineHeight(getSizeInDp( + R.dimen.name_text_size_for_small)))); + int avatarWidthSpace = mWidth - FIXED_WIDTH_DIMENS_FOR_SMALL; + avatarSize = Math.min(avatarHeightSpace, avatarWidthSpace); + } + + if (layoutId == R.layout.people_tile_large_with_content) { + avatarSize = mHeight - (FIXED_HEIGHT_DIMENS_FOR_LARGE_CONTENT + (getLineHeight( + getSizeInDp(R.dimen.content_text_size_for_large)) + * 3)); + return Math.min(avatarSize, getSizeInDp( + R.dimen.max_people_avatar_size_for_large_content)); + } + + if (layoutId == R.layout.people_tile_large_empty) { + int avatarHeightSpace = mHeight - (14 + 14 + getLineHeight( + getSizeInDp(R.dimen.name_text_size_for_large)) + + getLineHeight( + getSizeInDp(R.dimen.content_text_size_for_large)) + + 16 + 10 + 14); + int avatarWidthSpace = mWidth - (14 + 14); + avatarSize = Math.min(avatarHeightSpace, avatarWidthSpace); + } + return Math.min(avatarSize, + getSizeInDp(R.dimen.max_people_avatar_size)); + } + + private RemoteViews setCommonRemoteViewsFields(RemoteViews views, + int maxAvatarSize) { + try { + boolean isAvailable = + mTile.getStatuses() != null && mTile.getStatuses().stream().anyMatch( + c -> c.getAvailability() == AVAILABILITY_AVAILABLE); + if (isAvailable) { + views.setViewVisibility(R.id.availability, View.VISIBLE); + } else { + views.setViewVisibility(R.id.availability, View.GONE); + } + boolean hasNewStory = + mTile.getStatuses() != null && mTile.getStatuses().stream().anyMatch( + c -> c.getActivity() == ACTIVITY_NEW_STORY); + views.setTextViewText(R.id.name, mTile.getUserName().toString()); + views.setBoolean(R.id.image, "setClipToOutline", true); + + Icon icon = mTile.getUserIcon(); + PeopleStoryIconFactory storyIcon = new PeopleStoryIconFactory(mContext, + mContext.getPackageManager(), + IconDrawableFactory.newInstance(mContext, false), + maxAvatarSize); + Drawable drawable = icon.loadDrawable(mContext); + Drawable personDrawable = storyIcon.getPeopleTileDrawable(drawable, + mTile.getPackageName(), getUserId(mTile), mTile.isImportantConversation(), + hasNewStory); + Bitmap bitmap = convertDrawableToBitmap(personDrawable); + views.setImageViewBitmap(R.id.person_icon, bitmap); + + return views; + } catch (Exception e) { + Log.e(TAG, "Failed to set common fields: " + e); + } + return views; + } + + private RemoteViews setLaunchIntents(RemoteViews views) { + try { + Intent activityIntent = new Intent(mContext, LaunchConversationActivity.class); + activityIntent.addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_CLEAR_TASK + | Intent.FLAG_ACTIVITY_NO_HISTORY + | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + activityIntent.putExtra(PeopleSpaceWidgetProvider.EXTRA_TILE_ID, mTile.getId()); + activityIntent.putExtra( + PeopleSpaceWidgetProvider.EXTRA_PACKAGE_NAME, mTile.getPackageName()); + activityIntent.putExtra(PeopleSpaceWidgetProvider.EXTRA_USER_HANDLE, + mTile.getUserHandle()); + activityIntent.putExtra( + PeopleSpaceWidgetProvider.EXTRA_NOTIFICATION_KEY, mTile.getNotificationKey()); + views.setOnClickPendingIntent(R.id.item, PendingIntent.getActivity( + mContext, + mAppWidgetId, + activityIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE)); + return views; + } catch (Exception e) { + Log.e(TAG, "Failed to add launch intents: " + e); + } + + return views; + } + + private RemoteViews createMissedCallRemoteViews() { + RemoteViews views = getViewForContentLayout(); + views.setViewVisibility(R.id.predefined_icon, View.VISIBLE); + setMaxLines(views); + views.setTextViewText(R.id.text_content, mTile.getNotificationContent()); + views.setImageViewResource(R.id.predefined_icon, R.drawable.ic_phone_missed); + return views; + } + + private RemoteViews createNotificationRemoteViews() { + RemoteViews views = getViewForContentLayout(); + Uri image = mTile.getNotificationDataUri(); + if (image != null) { + // TODO: Use NotificationInlineImageCache + views.setImageViewUri(R.id.image, image); + views.setViewVisibility(R.id.image, View.VISIBLE); + views.setViewVisibility(R.id.text_content, View.GONE); + views.setImageViewResource(R.id.predefined_icon, R.drawable.ic_photo_camera); + } else { + setMaxLines(views); + CharSequence content = mTile.getNotificationContent(); + views = setPunctuationRemoteViewsFields(views, content); + TypedValue typedValue = new TypedValue(); + mContext.getTheme().resolveAttribute(android.R.attr.textColorPrimary, typedValue, true); + int primaryTextColor = mContext.getColor(typedValue.resourceId); + views.setInt(R.id.text_content, "setTextColor", primaryTextColor); + views.setTextViewText(R.id.text_content, mTile.getNotificationContent()); + views.setViewVisibility(R.id.image, View.GONE); + views.setImageViewResource(R.id.predefined_icon, R.drawable.ic_message); + } + // TODO: Set subtext as Group Sender name once storing the name in PeopleSpaceTile and + // subtract 1 from maxLines when present. + views.setViewVisibility(R.id.subtext, View.GONE); + return views; + } + + private RemoteViews createStatusRemoteViews(ConversationStatus status) { + RemoteViews views = getViewForContentLayout(); + CharSequence statusText = status.getDescription(); + if (TextUtils.isEmpty(statusText)) { + statusText = getStatusTextByType(status.getActivity()); + } + views.setViewVisibility(R.id.predefined_icon, View.VISIBLE); + setMaxLines(views); + // Secondary text color for statuses. + TypedValue typedValue = new TypedValue(); + mContext.getTheme().resolveAttribute(android.R.attr.textColorSecondary, typedValue, true); + int secondaryTextColor = mContext.getColor(typedValue.resourceId); + views.setInt(R.id.text_content, "setTextColor", secondaryTextColor); + views.setTextViewText(R.id.text_content, statusText); + + Icon statusIcon = status.getIcon(); + if (statusIcon != null) { + // No multi-line text with status images on medium layout. + views.setViewVisibility(R.id.text_content, View.GONE); + // Show 1-line subtext on large layout with status images. + if (mLayoutSize == LAYOUT_LARGE) { + views.setViewVisibility(R.id.subtext, View.VISIBLE); + views.setTextViewText(R.id.subtext, statusText); + } + views.setViewVisibility(R.id.image, View.VISIBLE); + views.setImageViewIcon(R.id.image, statusIcon); + } else { + views.setViewVisibility(R.id.image, View.GONE); + } + // TODO: Set status pre-defined icons + views.setImageViewResource(R.id.predefined_icon, R.drawable.ic_person); + return views; + } + + @Nullable + private ConversationStatus getBirthdayStatus( + List statuses) { + Optional birthdayStatus = statuses.stream().filter( + c -> c.getActivity() == ACTIVITY_BIRTHDAY).findFirst(); + if (birthdayStatus.isPresent()) { + return birthdayStatus.get(); + } + if (!TextUtils.isEmpty(mTile.getBirthdayText())) { + return new ConversationStatus.Builder(mTile.getId(), ACTIVITY_BIRTHDAY).build(); + } + + return null; + } + + /** + * Returns whether a {@code status} should have its own entire templated view. + * + *

A status may still be shown on the view (for example, as a new story ring) even if it's + * not valid to compose an entire view. + */ + private boolean isStatusValidForEntireStatusView(ConversationStatus status) { + switch (status.getActivity()) { + // Birthday & Anniversary don't require text provided or icon provided. + case ACTIVITY_BIRTHDAY: + case ACTIVITY_ANNIVERSARY: + return true; + default: + // For future birthday, location, new story, video, music, game, and other, the + // app must provide either text or an icon. + return !TextUtils.isEmpty(status.getDescription()) + || status.getIcon() != null; + } + } + + private String getStatusTextByType(int activity) { + switch (activity) { + case ACTIVITY_BIRTHDAY: + return mContext.getString(R.string.birthday_status); + case ACTIVITY_UPCOMING_BIRTHDAY: + return mContext.getString(R.string.upcoming_birthday_status); + case ACTIVITY_ANNIVERSARY: + return mContext.getString(R.string.anniversary_status); + case ACTIVITY_LOCATION: + return mContext.getString(R.string.location_status); + case ACTIVITY_NEW_STORY: + return mContext.getString(R.string.new_story_status); + case ACTIVITY_VIDEO: + return mContext.getString(R.string.video_status); + case ACTIVITY_AUDIO: + return mContext.getString(R.string.audio_status); + case ACTIVITY_GAME: + return mContext.getString(R.string.game_status); + default: + return EMPTY_STRING; + } + } + + private RemoteViews setPunctuationRemoteViewsFields( + RemoteViews views, CharSequence content) { + String punctuation = getBackgroundTextFromMessage(content.toString()); + int visibility = View.GONE; + if (punctuation != null) { + visibility = View.VISIBLE; + } + views.setTextViewText(R.id.punctuation1, punctuation); + views.setTextViewText(R.id.punctuation2, punctuation); + views.setTextViewText(R.id.punctuation3, punctuation); + views.setTextViewText(R.id.punctuation4, punctuation); + views.setTextViewText(R.id.punctuation5, punctuation); + views.setTextViewText(R.id.punctuation6, punctuation); + + views.setViewVisibility(R.id.punctuation1, visibility); + views.setViewVisibility(R.id.punctuation2, visibility); + views.setViewVisibility(R.id.punctuation3, visibility); + views.setViewVisibility(R.id.punctuation4, visibility); + views.setViewVisibility(R.id.punctuation5, visibility); + views.setViewVisibility(R.id.punctuation6, visibility); + + return views; + } + + /** Gets character for mTile background decoration based on notification content. */ + @VisibleForTesting + String getBackgroundTextFromMessage(String message) { + if (!ANY_DOUBLE_MARK_PATTERN.matcher(message).find()) { + return null; + } + if (MIXED_MARK_PATTERN.matcher(message).find()) { + return "!?"; + } + Matcher doubleQuestionMatcher = DOUBLE_QUESTION_PATTERN.matcher(message); + if (!doubleQuestionMatcher.find()) { + return "!"; + } + Matcher doubleExclamationMatcher = DOUBLE_EXCLAMATION_PATTERN.matcher(message); + if (!doubleExclamationMatcher.find()) { + return "?"; + } + // If we have both "!!" and "??", return the one that comes first. + if (doubleQuestionMatcher.start() < doubleExclamationMatcher.start()) { + return "?"; + } + return "!"; + } + + private RemoteViews getViewForContentLayout() { + RemoteViews views = new RemoteViews(mContext.getPackageName(), + getLayoutForContent()); + if (mLayoutSize == LAYOUT_SMALL) { + views.setViewVisibility(R.id.predefined_icon, View.VISIBLE); + views.setViewVisibility(R.id.name, View.GONE); + } else { + views.setViewVisibility(R.id.predefined_icon, View.GONE); + views.setViewVisibility(R.id.name, View.VISIBLE); + views.setViewVisibility(R.id.text_content, View.VISIBLE); + views.setViewVisibility(R.id.subtext, View.GONE); + } + return views; + } + + private RemoteViews createLastInteractionRemoteViews() { + RemoteViews views = new RemoteViews(mContext.getPackageName(), getEmptyLayout()); + if (mLayoutSize == LAYOUT_SMALL) { + views.setViewVisibility(R.id.name, View.VISIBLE); + views.setViewVisibility(R.id.predefined_icon, View.GONE); + } + String status = PeopleSpaceUtils.getLastInteractionString(mContext, + mTile.getLastInteractionTimestamp()); + views.setTextViewText(R.id.last_interaction, status); + return views; + } + + private int getEmptyLayout() { + switch (mLayoutSize) { + case LAYOUT_MEDIUM: + return R.layout.people_tile_medium_empty; + case LAYOUT_LARGE: + return R.layout.people_tile_large_empty; + case LAYOUT_SMALL: + default: + return R.layout.people_tile_small; + } + } + + private int getLayoutForContent() { + switch (mLayoutSize) { + case LAYOUT_MEDIUM: + return R.layout.people_tile_medium_with_content; + case LAYOUT_LARGE: + return R.layout.people_tile_large_with_content; + case LAYOUT_SMALL: + default: + return R.layout.people_tile_small; + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/PeopleSpaceUtilsTest.java b/packages/SystemUI/tests/src/com/android/systemui/people/PeopleSpaceUtilsTest.java index ee98a591c8a03..1c7a84a364046 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/people/PeopleSpaceUtilsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/people/PeopleSpaceUtilsTest.java @@ -16,20 +16,11 @@ package com.android.systemui.people; -import static android.app.Notification.CATEGORY_MISSED_CALL; -import static android.app.people.ConversationStatus.ACTIVITY_BIRTHDAY; -import static android.app.people.ConversationStatus.ACTIVITY_GAME; -import static android.app.people.ConversationStatus.ACTIVITY_NEW_STORY; -import static android.app.people.ConversationStatus.AVAILABILITY_AVAILABLE; -import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH; - import static com.android.systemui.people.PeopleSpaceUtils.PACKAGE_NAME; -import static com.android.systemui.people.PeopleSpaceUtils.REQUIRED_WIDTH_FOR_MEDIUM; import static com.android.systemui.people.widget.AppWidgetOptionsHelper.OPTIONS_PEOPLE_TILE; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -48,7 +39,6 @@ import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.Person; import android.app.people.ConversationChannel; -import android.app.people.ConversationStatus; import android.app.people.IPeopleManager; import android.app.people.PeopleSpaceTile; import android.appwidget.AppWidgetManager; @@ -70,9 +60,7 @@ import android.provider.ContactsContract; import android.service.notification.ConversationChannelWrapper; import android.service.notification.StatusBarNotification; import android.testing.AndroidTestingRunner; -import android.view.View; -import android.widget.RemoteViews; -import android.widget.TextView; +import android.util.DisplayMetrics; import androidx.test.filters.SmallTest; @@ -117,8 +105,6 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { private static final int TEST_COLUMN_INDEX = 1; private static final Uri URI = Uri.parse("fake_uri"); private static final Icon ICON = Icon.createWithResource("package", R.drawable.ic_android); - private static final String GAME_DESCRIPTION = "Playing a game!"; - private static final CharSequence MISSED_CALL = "Custom missed call message"; private static final String NAME = "username"; private static final Person PERSON = new Person.Builder() .setName("name") @@ -126,11 +112,6 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { .setUri(URI.toString()) .setBot(false) .build(); - private static final PeopleSpaceTile PERSON_TILE_WITHOUT_NOTIFICATION = - new PeopleSpaceTile - .Builder(SHORTCUT_ID_1, NAME, ICON, new Intent()) - .setLastInteractionTimestamp(0L) - .build(); private static final PeopleSpaceTile PERSON_TILE = new PeopleSpaceTile .Builder(SHORTCUT_ID_1, NAME, ICON, new Intent()) @@ -139,16 +120,6 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { .setNotificationContent(NOTIFICATION_CONTENT) .setNotificationDataUri(URI) .build(); - private static final ConversationStatus GAME_STATUS = - new ConversationStatus - .Builder(PERSON_TILE.getId(), ACTIVITY_GAME) - .setDescription(GAME_DESCRIPTION) - .build(); - private static final ConversationStatus NEW_STORY_WITH_AVAILABILITY = - new ConversationStatus - .Builder(PERSON_TILE.getId(), ACTIVITY_NEW_STORY) - .setAvailability(AVAILABILITY_AVAILABLE) - .build(); private final ShortcutInfo mShortcutInfo = new ShortcutInfo.Builder(mContext, SHORTCUT_ID_1).setLongLabel( @@ -244,6 +215,12 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { when(mAppWidgetManager.getAppWidgetOptions(eq(WIDGET_ID_WITHOUT_SHORTCUT))) .thenReturn(new Bundle()); + Configuration configuration = mock(Configuration.class); + DisplayMetrics displayMetrics = mock(DisplayMetrics.class); + Resources resources = mock(Resources.class); + when(mMockContext.getResources()).thenReturn(resources); + when(resources.getConfiguration()).thenReturn(configuration); + when(resources.getDisplayMetrics()).thenReturn(displayMetrics); when(mMockContext.getContentResolver()).thenReturn(mMockContentResolver); when(mMockContentResolver.query(any(Uri.class), any(), anyString(), any(), isNull())).thenReturn(mMockCursor); @@ -254,10 +231,6 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { when(mMockContext.getPackageManager()).thenReturn(mPackageManager); when(mMockContext.getString(R.string.over_timestamp)).thenReturn( mContext.getString(R.string.over_timestamp)); - Configuration configuration = mock(Configuration.class); - Resources resources = mock(Resources.class); - when(mMockContext.getResources()).thenReturn(resources); - when(resources.getConfiguration()).thenReturn(configuration); when(mPackageManager.getApplicationIcon(anyString())).thenReturn(null); when(mNotificationEntryManager.getVisibleNotifications()) .thenReturn(List.of(mNotificationEntry1, mNotificationEntry2, mNotificationEntry3)); @@ -361,95 +334,6 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { assertThat(lastMessage).isNull(); } - @Test - public void testGetBackgroundTextFromMessageNoPunctuation() { - String backgroundText = PeopleSpaceUtils.getBackgroundTextFromMessage("test"); - - assertThat(backgroundText).isNull(); - } - - @Test - public void testGetBackgroundTextFromMessageSingleExclamation() { - String backgroundText = PeopleSpaceUtils.getBackgroundTextFromMessage("test!"); - - assertThat(backgroundText).isNull(); - } - - @Test - public void testGetBackgroundTextFromMessageSingleQuestion() { - String backgroundText = PeopleSpaceUtils.getBackgroundTextFromMessage("?test"); - - assertThat(backgroundText).isNull(); - } - - @Test - public void testGetBackgroundTextFromMessageSeparatedMarks() { - String backgroundText = PeopleSpaceUtils.getBackgroundTextFromMessage("test! right!"); - - assertThat(backgroundText).isNull(); - } - - @Test - public void testGetBackgroundTextFromMessageDoubleExclamation() { - String backgroundText = PeopleSpaceUtils.getBackgroundTextFromMessage("!!test"); - - assertThat(backgroundText).isEqualTo("!"); - } - - @Test - public void testGetBackgroundTextFromMessageDoubleQuestion() { - String backgroundText = PeopleSpaceUtils.getBackgroundTextFromMessage("test??"); - - assertThat(backgroundText).isEqualTo("?"); - } - - @Test - public void testGetBackgroundTextFromMessageMixed() { - String backgroundText = PeopleSpaceUtils.getBackgroundTextFromMessage("test?!"); - - assertThat(backgroundText).isEqualTo("!?"); - } - - @Test - public void testGetBackgroundTextFromMessageMixedInTheMiddle() { - String backgroundText = PeopleSpaceUtils.getBackgroundTextFromMessage( - "test!? in the middle"); - - assertThat(backgroundText).isEqualTo("!?"); - } - - @Test - public void testGetBackgroundTextFromMessageMixedDifferentOrder() { - String backgroundText = PeopleSpaceUtils.getBackgroundTextFromMessage( - "test!? in the middle"); - - assertThat(backgroundText).isEqualTo("!?"); - } - - @Test - public void testGetBackgroundTextFromMessageMultiple() { - String backgroundText = PeopleSpaceUtils.getBackgroundTextFromMessage( - "test!?!!? in the middle"); - - assertThat(backgroundText).isEqualTo("!?"); - } - - @Test - public void testGetBackgroundTextFromMessageQuestionFirst() { - String backgroundText = PeopleSpaceUtils.getBackgroundTextFromMessage( - "test?? in the middle!!"); - - assertThat(backgroundText).isEqualTo("?"); - } - - @Test - public void testGetBackgroundTextFromMessageExclamationFirst() { - String backgroundText = PeopleSpaceUtils.getBackgroundTextFromMessage( - "test!! in the middle??"); - - assertThat(backgroundText).isEqualTo("!"); - } - @Test public void testGetLastMessagingStyleMessage() { StatusBarNotification sbn = new SbnBuilder() @@ -687,225 +571,6 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { any()); } - @Test - public void testCreateRemoteViewsWithLastInteractionTime() { - RemoteViews views = PeopleSpaceUtils.createRemoteViews(mMockContext, - PERSON_TILE_WITHOUT_NOTIFICATION, 0, mOptions); - View result = views.apply(mContext, null); - - TextView name = (TextView) result.findViewById(R.id.name); - assertEquals(name.getText(), NAME); - // Has last interaction. - TextView lastInteraction = (TextView) result.findViewById(R.id.last_interaction); - assertEquals(lastInteraction.getText(), mContext.getString(R.string.basic_status)); - // No availability. - View availability = result.findViewById(R.id.availability); - assertEquals(View.GONE, availability.getVisibility()); - // Shows person icon. - View personIcon = result.findViewById(R.id.person_icon); - assertEquals(View.VISIBLE, personIcon.getVisibility()); - // No status. - assertThat((View) result.findViewById(R.id.text_content)).isNull(); - - mOptions.putInt(OPTION_APPWIDGET_MIN_WIDTH, REQUIRED_WIDTH_FOR_MEDIUM - 1); - RemoteViews smallView = PeopleSpaceUtils.createRemoteViews(mContext, - PERSON_TILE_WITHOUT_NOTIFICATION, 0, mOptions); - View smallResult = smallView.apply(mContext, null); - - // Show name over predefined icon. - assertEquals(View.VISIBLE, smallResult.findViewById(R.id.name).getVisibility()); - assertEquals(View.GONE, smallResult.findViewById(R.id.predefined_icon).getVisibility()); - // Shows person icon. - assertEquals(View.VISIBLE, smallResult.findViewById(R.id.person_icon).getVisibility()); - } - - @Test - public void testCreateRemoteViewsWithGameTypeOnlyIsIgnored() { - PeopleSpaceTile tileWithAvailabilityAndNewStory = - PERSON_TILE_WITHOUT_NOTIFICATION.toBuilder().setStatuses( - Arrays.asList(NEW_STORY_WITH_AVAILABILITY, - new ConversationStatus.Builder( - PERSON_TILE_WITHOUT_NOTIFICATION.getId(), - ACTIVITY_GAME).build())).build(); - RemoteViews views = PeopleSpaceUtils.createRemoteViews(mMockContext, - tileWithAvailabilityAndNewStory, 0, mOptions); - View result = views.apply(mContext, null); - - TextView name = (TextView) result.findViewById(R.id.name); - assertEquals(name.getText(), NAME); - // Has last interaction over status. - TextView lastInteraction = (TextView) result.findViewById(R.id.last_interaction); - assertEquals(lastInteraction.getText(), mContext.getString(R.string.basic_status)); - // Has availability. - View availability = result.findViewById(R.id.availability); - assertEquals(View.VISIBLE, availability.getVisibility()); - // Has person icon. - View personIcon = result.findViewById(R.id.person_icon); - assertEquals(View.VISIBLE, personIcon.getVisibility()); - // No status. - assertThat((View) result.findViewById(R.id.text_content)).isNull(); - - mOptions.putInt(OPTION_APPWIDGET_MIN_WIDTH, REQUIRED_WIDTH_FOR_MEDIUM - 1); - RemoteViews smallView = PeopleSpaceUtils.createRemoteViews(mContext, - tileWithAvailabilityAndNewStory, 0, mOptions); - View smallResult = smallView.apply(mContext, null); - - // Show name rather than game type. - assertEquals(View.VISIBLE, smallResult.findViewById(R.id.name).getVisibility()); - assertEquals(View.GONE, smallResult.findViewById(R.id.predefined_icon).getVisibility()); - // Has person icon. - assertEquals(View.VISIBLE, - smallResult.findViewById(R.id.person_icon).getVisibility()); - } - - @Test - public void testCreateRemoteViewsWithBirthdayTypeOnlyIsNotIgnored() { - PeopleSpaceTile tileWithStatusTemplate = - PERSON_TILE_WITHOUT_NOTIFICATION.toBuilder().setStatuses( - Arrays.asList( - NEW_STORY_WITH_AVAILABILITY, new ConversationStatus.Builder( - PERSON_TILE_WITHOUT_NOTIFICATION.getId(), - ACTIVITY_BIRTHDAY).build())).build(); - RemoteViews views = PeopleSpaceUtils.createRemoteViews(mContext, - tileWithStatusTemplate, 0, mOptions); - View result = views.apply(mContext, null); - - TextView name = (TextView) result.findViewById(R.id.name); - assertEquals(name.getText(), NAME); - // Has availability. - View availability = result.findViewById(R.id.availability); - assertEquals(View.VISIBLE, availability.getVisibility()); - // Has person icon. - View personIcon = result.findViewById(R.id.person_icon); - assertEquals(View.VISIBLE, personIcon.getVisibility()); - // Has status text from backup text. - TextView statusContent = (TextView) result.findViewById(R.id.text_content); - assertEquals(statusContent.getText(), mContext.getString(R.string.birthday_status)); - - mOptions.putInt(OPTION_APPWIDGET_MIN_WIDTH, REQUIRED_WIDTH_FOR_MEDIUM - 1); - RemoteViews smallView = PeopleSpaceUtils.createRemoteViews(mContext, - tileWithStatusTemplate, 0, mOptions); - View smallResult = smallView.apply(mContext, null); - - // Show icon instead of name. - assertEquals(View.GONE, smallResult.findViewById(R.id.name).getVisibility()); - assertEquals(View.VISIBLE, - smallResult.findViewById(R.id.predefined_icon).getVisibility()); - // Has person icon. - assertEquals(View.VISIBLE, - smallResult.findViewById(R.id.person_icon).getVisibility()); - } - - @Test - public void testCreateRemoteViewsWithStatusTemplate() { - PeopleSpaceTile tileWithStatusTemplate = - PERSON_TILE_WITHOUT_NOTIFICATION.toBuilder().setStatuses( - Arrays.asList(GAME_STATUS, - NEW_STORY_WITH_AVAILABILITY)).build(); - RemoteViews views = PeopleSpaceUtils.createRemoteViews(mContext, - tileWithStatusTemplate, 0, mOptions); - View result = views.apply(mContext, null); - - TextView name = (TextView) result.findViewById(R.id.name); - assertEquals(name.getText(), NAME); - // Has availability. - View availability = result.findViewById(R.id.availability); - assertEquals(View.VISIBLE, availability.getVisibility()); - // Has person icon. - View personIcon = result.findViewById(R.id.person_icon); - assertEquals(View.VISIBLE, personIcon.getVisibility()); - // Has status. - TextView statusContent = (TextView) result.findViewById(R.id.text_content); - assertEquals(statusContent.getText(), GAME_DESCRIPTION); - - mOptions.putInt(OPTION_APPWIDGET_MIN_WIDTH, REQUIRED_WIDTH_FOR_MEDIUM - 1); - RemoteViews smallView = PeopleSpaceUtils.createRemoteViews(mContext, - tileWithStatusTemplate, 0, mOptions); - View smallResult = smallView.apply(mContext, null); - - // Show icon instead of name. - assertEquals(View.GONE, smallResult.findViewById(R.id.name).getVisibility()); - assertEquals(View.VISIBLE, - smallResult.findViewById(R.id.predefined_icon).getVisibility()); - // Has person icon. - assertEquals(View.VISIBLE, - smallResult.findViewById(R.id.person_icon).getVisibility()); - } - - @Test - public void testCreateRemoteViewsWithMissedCallNotification() { - PeopleSpaceTile tileWithMissedCallNotification = PERSON_TILE.toBuilder() - .setNotificationDataUri(null) - .setNotificationCategory(CATEGORY_MISSED_CALL) - .setNotificationContent(MISSED_CALL) - .build(); - RemoteViews views = PeopleSpaceUtils.createRemoteViews(mContext, - tileWithMissedCallNotification, 0, mOptions); - View result = views.apply(mContext, null); - - TextView name = (TextView) result.findViewById(R.id.name); - assertEquals(name.getText(), NAME); - // Has availability. - View availability = result.findViewById(R.id.availability); - assertEquals(View.GONE, availability.getVisibility()); - // Has person icon. - View personIcon = result.findViewById(R.id.person_icon); - assertEquals(View.VISIBLE, personIcon.getVisibility()); - // Has missed call notification content. - TextView statusContent = (TextView) result.findViewById(R.id.text_content); - assertEquals(statusContent.getText(), MISSED_CALL); - - mOptions.putInt(OPTION_APPWIDGET_MIN_WIDTH, REQUIRED_WIDTH_FOR_MEDIUM - 1); - RemoteViews smallView = PeopleSpaceUtils.createRemoteViews(mContext, - tileWithMissedCallNotification, 0, mOptions); - View smallResult = smallView.apply(mContext, null); - - // Show icon instead of name. - assertEquals(View.GONE, smallResult.findViewById(R.id.name).getVisibility()); - assertEquals(View.VISIBLE, - smallResult.findViewById(R.id.predefined_icon).getVisibility()); - // Has person icon. - assertEquals(View.VISIBLE, smallResult.findViewById(R.id.person_icon).getVisibility()); - } - - @Test - public void testCreateRemoteViewsWithNotificationTemplate() { - PeopleSpaceTile tileWithStatusAndNotification = PERSON_TILE.toBuilder() - .setNotificationDataUri(null) - .setStatuses(Arrays.asList(GAME_STATUS, - NEW_STORY_WITH_AVAILABILITY)).build(); - RemoteViews views = PeopleSpaceUtils.createRemoteViews(mContext, - tileWithStatusAndNotification, 0, mOptions); - View result = views.apply(mContext, null); - - TextView name = (TextView) result.findViewById(R.id.name); - assertEquals(name.getText(), NAME); - TextView subtext = (TextView) result.findViewById(R.id.subtext); - assertEquals(View.GONE, subtext.getVisibility()); - // Has availability. - View availability = result.findViewById(R.id.availability); - assertEquals(View.VISIBLE, availability.getVisibility()); - // Has person icon. - View personIcon = result.findViewById(R.id.person_icon); - assertEquals(View.VISIBLE, personIcon.getVisibility()); - // Has notification content. - TextView statusContent = (TextView) result.findViewById(R.id.text_content); - assertEquals(statusContent.getText(), NOTIFICATION_CONTENT); - - mOptions.putInt(OPTION_APPWIDGET_MIN_WIDTH, REQUIRED_WIDTH_FOR_MEDIUM - 1); - RemoteViews smallView = PeopleSpaceUtils.createRemoteViews(mContext, - tileWithStatusAndNotification, 0, mOptions); - View smallResult = smallView.apply(mContext, null); - - // Show icon instead of name. - assertEquals(View.GONE, smallResult.findViewById(R.id.name).getVisibility()); - assertEquals(View.VISIBLE, - smallResult.findViewById(R.id.predefined_icon).getVisibility()); - // Has person icon. - assertEquals(View.VISIBLE, - smallResult.findViewById(R.id.person_icon).getVisibility()); - } - private ConversationChannelWrapper getConversationChannelWrapper(String shortcutId, boolean importantConversation, long lastInteractionTimestamp) throws Exception { ConversationChannelWrapper convo = new ConversationChannelWrapper(); @@ -931,13 +596,4 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { eq(shortcutId))).thenReturn(lastInteractionTimestamp); return convo; } - - private ConversationChannel getConversationChannelWithoutTimestamp(String shortcutId) - throws Exception { - ShortcutInfo shortcutInfo = new ShortcutInfo.Builder(mContext, shortcutId).setLongLabel( - "name").build(); - ConversationChannel convo = new ConversationChannel(shortcutInfo, 0, null, null, - 0L, false); - return convo; - } -} \ No newline at end of file +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/PeopleTileViewHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/people/PeopleTileViewHelperTest.java new file mode 100644 index 0000000000000..39bf060507419 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/people/PeopleTileViewHelperTest.java @@ -0,0 +1,587 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.people; + +import static android.app.Notification.CATEGORY_MISSED_CALL; +import static android.app.people.ConversationStatus.ACTIVITY_BIRTHDAY; +import static android.app.people.ConversationStatus.ACTIVITY_GAME; +import static android.app.people.ConversationStatus.ACTIVITY_NEW_STORY; +import static android.app.people.ConversationStatus.AVAILABILITY_AVAILABLE; +import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH; + +import static com.android.systemui.people.widget.AppWidgetOptionsHelper.OPTIONS_PEOPLE_TILE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.app.people.ConversationStatus; +import android.app.people.PeopleSpaceTile; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.drawable.Icon; +import android.net.Uri; +import android.os.Bundle; +import android.testing.AndroidTestingRunner; +import android.util.DisplayMetrics; +import android.view.View; +import android.widget.RemoteViews; +import android.widget.TextView; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.R; +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; + +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class PeopleTileViewHelperTest extends SysuiTestCase { + + private static final String SHORTCUT_ID_1 = "101"; + private static final String NOTIFICATION_KEY = "notification_key"; + private static final String NOTIFICATION_CONTENT = "notification_content"; + private static final Uri URI = Uri.parse("fake_uri"); + private static final Icon ICON = Icon.createWithResource("package", R.drawable.ic_android); + private static final String GAME_DESCRIPTION = "Playing a game!"; + private static final CharSequence MISSED_CALL = "Custom missed call message"; + private static final String NAME = "username"; + private static final PeopleSpaceTile PERSON_TILE_WITHOUT_NOTIFICATION = + new PeopleSpaceTile + .Builder(SHORTCUT_ID_1, NAME, ICON, new Intent()) + .setLastInteractionTimestamp(0L) + .build(); + private static final PeopleSpaceTile PERSON_TILE = + new PeopleSpaceTile + .Builder(SHORTCUT_ID_1, NAME, ICON, new Intent()) + .setLastInteractionTimestamp(123L) + .setNotificationKey(NOTIFICATION_KEY) + .setNotificationContent(NOTIFICATION_CONTENT) + .setNotificationDataUri(URI) + .build(); + private static final ConversationStatus GAME_STATUS = + new ConversationStatus + .Builder(PERSON_TILE.getId(), ACTIVITY_GAME) + .setDescription(GAME_DESCRIPTION) + .build(); + private static final ConversationStatus NEW_STORY_WITH_AVAILABILITY = + new ConversationStatus + .Builder(PERSON_TILE.getId(), ACTIVITY_NEW_STORY) + .setAvailability(AVAILABILITY_AVAILABLE) + .build(); + + @Mock + private Context mMockContext; + @Mock + private PackageManager mPackageManager; + + private Bundle mOptions; + private PeopleTileViewHelper mPeopleTileViewHelper; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mOptions = new Bundle(); + mOptions.putParcelable(OPTIONS_PEOPLE_TILE, PERSON_TILE); + + when(mMockContext.getString(R.string.birthday_status)).thenReturn( + mContext.getString(R.string.birthday_status)); + when(mMockContext.getString(R.string.basic_status)).thenReturn( + mContext.getString(R.string.basic_status)); + when(mMockContext.getPackageManager()).thenReturn(mPackageManager); + when(mMockContext.getString(R.string.over_timestamp)).thenReturn( + mContext.getString(R.string.over_timestamp)); + Configuration configuration = mock(Configuration.class); + DisplayMetrics displayMetrics = mock(DisplayMetrics.class); + Resources resources = mock(Resources.class); + when(mMockContext.getResources()).thenReturn(resources); + when(resources.getConfiguration()).thenReturn(configuration); + when(resources.getDisplayMetrics()).thenReturn(displayMetrics); + TextView textView = mock(TextView.class); + // when(new TextView(mMockContext)).thenReturn(textView); + when(textView.getLineHeight()).thenReturn(16); + when(mPackageManager.getApplicationIcon(anyString())).thenReturn(null); + mPeopleTileViewHelper = new PeopleTileViewHelper(mContext, + PERSON_TILE, 0, mOptions); + } + + @Test + public void testCreateRemoteViewsWithLastInteractionTime() { + RemoteViews views = new PeopleTileViewHelper(mContext, + PERSON_TILE_WITHOUT_NOTIFICATION, 0, mOptions).getViews(); + View result = views.apply(mContext, null); + + TextView name = (TextView) result.findViewById(R.id.name); + assertEquals(name.getText(), NAME); + // Has last interaction. + TextView lastInteraction = (TextView) result.findViewById(R.id.last_interaction); + assertEquals(lastInteraction.getText(), mContext.getString(R.string.basic_status)); + // No availability. + assertEquals(View.GONE, result.findViewById(R.id.availability).getVisibility()); + // Shows person icon. + assertEquals(View.VISIBLE, result.findViewById(R.id.person_icon).getVisibility()); + // No status. + assertThat((View) result.findViewById(R.id.text_content)).isNull(); + + mOptions.putInt(OPTION_APPWIDGET_MIN_WIDTH, + getSizeInDp(R.dimen.required_width_for_medium) - 1); + RemoteViews smallView = new PeopleTileViewHelper(mContext, + PERSON_TILE_WITHOUT_NOTIFICATION, 0, mOptions).getViews(); + View smallResult = smallView.apply(mContext, null); + + // Show name over predefined icon. + assertEquals(View.VISIBLE, smallResult.findViewById(R.id.name).getVisibility()); + assertEquals(View.GONE, smallResult.findViewById(R.id.predefined_icon).getVisibility()); + // Shows person icon. + assertEquals(View.VISIBLE, smallResult.findViewById(R.id.person_icon).getVisibility()); + + mOptions.putInt(OPTION_APPWIDGET_MIN_WIDTH, + getSizeInDp(R.dimen.required_width_for_large)); + mOptions.putInt(OPTION_APPWIDGET_MIN_WIDTH, + getSizeInDp(R.dimen.required_height_for_large)); + RemoteViews largeView = new PeopleTileViewHelper(mContext, + PERSON_TILE_WITHOUT_NOTIFICATION, 0, mOptions).getViews(); + View largeResult = largeView.apply(mContext, null); + + name = (TextView) largeResult.findViewById(R.id.name); + assertEquals(name.getText(), NAME); + // Has last interaction. + lastInteraction = (TextView) result.findViewById(R.id.last_interaction); + assertEquals(lastInteraction.getText(), mContext.getString(R.string.basic_status)); + // No availability. + assertEquals(View.GONE, result.findViewById(R.id.availability).getVisibility()); + // Shows person icon. + assertEquals(View.VISIBLE, result.findViewById(R.id.person_icon).getVisibility()); + // No status. + assertThat((View) result.findViewById(R.id.text_content)).isNull(); + } + + @Test + public void testCreateRemoteViewsWithGameTypeOnlyIsIgnored() { + PeopleSpaceTile tileWithAvailabilityAndNewStory = + PERSON_TILE_WITHOUT_NOTIFICATION.toBuilder().setStatuses( + Arrays.asList(NEW_STORY_WITH_AVAILABILITY, + new ConversationStatus.Builder( + PERSON_TILE_WITHOUT_NOTIFICATION.getId(), + ACTIVITY_GAME).build())).build(); + RemoteViews views = new PeopleTileViewHelper(mContext, + tileWithAvailabilityAndNewStory, 0, mOptions).getViews(); + View result = views.apply(mContext, null); + + TextView name = (TextView) result.findViewById(R.id.name); + assertEquals(name.getText(), NAME); + // Has last interaction over status. + TextView lastInteraction = (TextView) result.findViewById(R.id.last_interaction); + assertEquals(lastInteraction.getText(), mContext.getString(R.string.basic_status)); + // Has availability. + assertEquals(View.VISIBLE, result.findViewById(R.id.availability).getVisibility()); + // Has person icon. + assertEquals(View.VISIBLE, result.findViewById(R.id.person_icon).getVisibility()); + // No status. + assertThat((View) result.findViewById(R.id.text_content)).isNull(); + + mOptions.putInt(OPTION_APPWIDGET_MIN_WIDTH, + getSizeInDp(R.dimen.required_width_for_medium) - 1); + RemoteViews smallView = new PeopleTileViewHelper(mContext, + tileWithAvailabilityAndNewStory, 0, mOptions).getViews(); + View smallResult = smallView.apply(mContext, null); + + // Show name rather than game type. + assertEquals(View.VISIBLE, smallResult.findViewById(R.id.name).getVisibility()); + assertEquals(View.GONE, smallResult.findViewById(R.id.predefined_icon).getVisibility()); + // Has person icon. + assertEquals(View.VISIBLE, + smallResult.findViewById(R.id.person_icon).getVisibility()); + + mOptions.putInt(OPTION_APPWIDGET_MIN_WIDTH, + getSizeInDp(R.dimen.required_width_for_large)); + mOptions.putInt(OPTION_APPWIDGET_MIN_WIDTH, + getSizeInDp(R.dimen.required_height_for_large)); + RemoteViews largeView = new PeopleTileViewHelper(mContext, + tileWithAvailabilityAndNewStory, 0, mOptions).getViews(); + View largeResult = largeView.apply(mContext, null); + + name = (TextView) largeResult.findViewById(R.id.name); + assertEquals(name.getText(), NAME); + // Has last interaction. + lastInteraction = (TextView) result.findViewById(R.id.last_interaction); + assertEquals(lastInteraction.getText(), mContext.getString(R.string.basic_status)); + // Has availability. + assertEquals(View.VISIBLE, result.findViewById(R.id.availability).getVisibility()); + // Shows person icon. + assertEquals(View.VISIBLE, result.findViewById(R.id.person_icon).getVisibility()); + // No status. + assertThat((View) result.findViewById(R.id.text_content)).isNull(); + } + + @Test + public void testCreateRemoteViewsWithBirthdayTypeOnlyIsNotIgnored() { + PeopleSpaceTile tileWithStatusTemplate = + PERSON_TILE_WITHOUT_NOTIFICATION.toBuilder().setStatuses( + Arrays.asList( + NEW_STORY_WITH_AVAILABILITY, new ConversationStatus.Builder( + PERSON_TILE_WITHOUT_NOTIFICATION.getId(), + ACTIVITY_BIRTHDAY).build())).build(); + RemoteViews views = new PeopleTileViewHelper(mContext, + tileWithStatusTemplate, 0, mOptions).getViews(); + View result = views.apply(mContext, null); + + TextView name = (TextView) result.findViewById(R.id.name); + assertEquals(name.getText(), NAME); + assertEquals(View.GONE, result.findViewById(R.id.subtext).getVisibility()); + assertEquals(View.VISIBLE, result.findViewById(R.id.predefined_icon).getVisibility()); + // Has availability. + assertEquals(View.VISIBLE, result.findViewById(R.id.availability).getVisibility()); + // Has person icon. + assertEquals(View.VISIBLE, result.findViewById(R.id.person_icon).getVisibility()); + // Has status text from backup text. + TextView statusContent = (TextView) result.findViewById(R.id.text_content); + assertEquals(View.VISIBLE, statusContent.getVisibility()); + assertEquals(statusContent.getText(), mContext.getString(R.string.birthday_status)); + assertThat(statusContent.getMaxLines()).isEqualTo(3); + + mOptions.putInt(OPTION_APPWIDGET_MIN_WIDTH, + getSizeInDp(R.dimen.required_width_for_medium) - 1); + RemoteViews smallView = new PeopleTileViewHelper(mContext, + tileWithStatusTemplate, 0, mOptions).getViews(); + View smallResult = smallView.apply(mContext, null); + + // Show icon instead of name. + assertEquals(View.GONE, smallResult.findViewById(R.id.name).getVisibility()); + assertEquals(View.VISIBLE, + smallResult.findViewById(R.id.predefined_icon).getVisibility()); + // Has person icon. + assertEquals(View.VISIBLE, + smallResult.findViewById(R.id.person_icon).getVisibility()); + + mOptions.putInt(OPTION_APPWIDGET_MIN_WIDTH, + getSizeInDp(R.dimen.required_width_for_large)); + mOptions.putInt(OPTION_APPWIDGET_MIN_WIDTH, + getSizeInDp(R.dimen.required_height_for_large)); + RemoteViews largeView = new PeopleTileViewHelper(mContext, + tileWithStatusTemplate, 0, mOptions).getViews(); + View largeResult = largeView.apply(mContext, null); + + name = (TextView) largeResult.findViewById(R.id.name); + assertEquals(name.getText(), NAME); + assertEquals(View.GONE, largeResult.findViewById(R.id.subtext).getVisibility()); + assertEquals(View.VISIBLE, largeResult.findViewById(R.id.predefined_icon).getVisibility()); + // Has availability. + assertEquals(View.VISIBLE, largeResult.findViewById(R.id.availability).getVisibility()); + // Has person icon. + View personIcon = largeResult.findViewById(R.id.person_icon); + assertEquals(View.VISIBLE, personIcon.getVisibility()); + // Has notification content. + statusContent = (TextView) largeResult.findViewById(R.id.text_content); + assertEquals(View.VISIBLE, statusContent.getVisibility()); + assertEquals(statusContent.getText(), mContext.getString(R.string.birthday_status)); + assertThat(statusContent.getMaxLines()).isEqualTo(3); + } + + @Test + public void testCreateRemoteViewsWithStatusTemplate() { + PeopleSpaceTile tileWithStatusTemplate = + PERSON_TILE_WITHOUT_NOTIFICATION.toBuilder().setStatuses( + Arrays.asList(GAME_STATUS, + NEW_STORY_WITH_AVAILABILITY)).build(); + RemoteViews views = new PeopleTileViewHelper(mContext, + tileWithStatusTemplate, 0, mOptions).getViews(); + View result = views.apply(mContext, null); + + TextView name = (TextView) result.findViewById(R.id.name); + assertEquals(name.getText(), NAME); + assertEquals(View.GONE, result.findViewById(R.id.subtext).getVisibility()); + assertEquals(View.VISIBLE, result.findViewById(R.id.predefined_icon).getVisibility()); + // Has availability. + assertEquals(View.VISIBLE, result.findViewById(R.id.availability).getVisibility()); + // Has person icon. + assertEquals(View.VISIBLE, result.findViewById(R.id.person_icon).getVisibility()); + // Has status. + TextView statusContent = (TextView) result.findViewById(R.id.text_content); + assertEquals(statusContent.getText(), GAME_DESCRIPTION); + assertThat(statusContent.getMaxLines()).isEqualTo(3); + + mOptions.putInt(OPTION_APPWIDGET_MIN_WIDTH, + getSizeInDp(R.dimen.required_width_for_medium) - 1); + RemoteViews smallView = new PeopleTileViewHelper(mContext, + tileWithStatusTemplate, 0, mOptions).getViews(); + View smallResult = smallView.apply(mContext, null); + + // Show icon instead of name. + assertEquals(View.GONE, smallResult.findViewById(R.id.name).getVisibility()); + assertEquals(View.VISIBLE, + smallResult.findViewById(R.id.predefined_icon).getVisibility()); + // Has person icon. + assertEquals(View.VISIBLE, + smallResult.findViewById(R.id.person_icon).getVisibility()); + + mOptions.putInt(OPTION_APPWIDGET_MIN_WIDTH, + getSizeInDp(R.dimen.required_width_for_large)); + mOptions.putInt(OPTION_APPWIDGET_MIN_WIDTH, + getSizeInDp(R.dimen.required_height_for_large)); + RemoteViews largeView = new PeopleTileViewHelper(mContext, + tileWithStatusTemplate, 0, mOptions).getViews(); + View largeResult = largeView.apply(mContext, null); + + name = (TextView) largeResult.findViewById(R.id.name); + assertEquals(name.getText(), NAME); + assertEquals(View.GONE, largeResult.findViewById(R.id.subtext).getVisibility()); + assertEquals(View.VISIBLE, largeResult.findViewById(R.id.predefined_icon).getVisibility()); + // Has availability. + assertEquals(View.VISIBLE, largeResult.findViewById(R.id.availability).getVisibility()); + // Has person icon. + View personIcon = largeResult.findViewById(R.id.person_icon); + assertEquals(View.VISIBLE, personIcon.getVisibility()); + // Has notification content. + statusContent = (TextView) largeResult.findViewById(R.id.text_content); + assertEquals(View.VISIBLE, statusContent.getVisibility()); + assertEquals(statusContent.getText(), GAME_DESCRIPTION); + assertThat(statusContent.getMaxLines()).isEqualTo(3); + } + + @Test + public void testCreateRemoteViewsWithMissedCallNotification() { + PeopleSpaceTile tileWithMissedCallNotification = PERSON_TILE.toBuilder() + .setNotificationDataUri(null) + .setNotificationCategory(CATEGORY_MISSED_CALL) + .setNotificationContent(MISSED_CALL) + .build(); + RemoteViews views = new PeopleTileViewHelper(mContext, + tileWithMissedCallNotification, 0, mOptions).getViews(); + View result = views.apply(mContext, null); + + TextView name = (TextView) result.findViewById(R.id.name); + assertEquals(name.getText(), NAME); + assertEquals(View.GONE, result.findViewById(R.id.subtext).getVisibility()); + assertEquals(View.VISIBLE, result.findViewById(R.id.predefined_icon).getVisibility()); + // Has availability. + assertEquals(View.GONE, result.findViewById(R.id.availability).getVisibility()); + // Has person icon. + assertEquals(View.VISIBLE, result.findViewById(R.id.person_icon).getVisibility()); + // Has missed call notification content. + TextView statusContent = (TextView) result.findViewById(R.id.text_content); + assertEquals(View.VISIBLE, statusContent.getVisibility()); + assertEquals(statusContent.getText(), MISSED_CALL); + assertThat(statusContent.getMaxLines()).isEqualTo(3); + + mOptions.putInt(OPTION_APPWIDGET_MIN_WIDTH, + getSizeInDp(R.dimen.required_width_for_medium) - 1); + RemoteViews smallView = new PeopleTileViewHelper(mContext, + tileWithMissedCallNotification, 0, mOptions).getViews(); + View smallResult = smallView.apply(mContext, null); + + // Show icon instead of name. + assertEquals(View.GONE, smallResult.findViewById(R.id.name).getVisibility()); + assertEquals(View.VISIBLE, + smallResult.findViewById(R.id.predefined_icon).getVisibility()); + // Has person icon. + assertEquals(View.VISIBLE, smallResult.findViewById(R.id.person_icon).getVisibility()); + + mOptions.putInt(OPTION_APPWIDGET_MIN_WIDTH, + getSizeInDp(R.dimen.required_width_for_large)); + mOptions.putInt(OPTION_APPWIDGET_MIN_WIDTH, + getSizeInDp(R.dimen.required_height_for_large)); + RemoteViews largeView = new PeopleTileViewHelper(mContext, + tileWithMissedCallNotification, 0, mOptions).getViews(); + View largeResult = largeView.apply(mContext, null); + + name = (TextView) largeResult.findViewById(R.id.name); + assertEquals(name.getText(), NAME); + assertEquals(View.GONE, largeResult.findViewById(R.id.subtext).getVisibility()); + assertEquals(View.VISIBLE, largeResult.findViewById(R.id.predefined_icon).getVisibility()); + // Has availability. + assertEquals(View.GONE, largeResult.findViewById(R.id.availability).getVisibility()); + // Has person icon. + View personIcon = largeResult.findViewById(R.id.person_icon); + assertEquals(View.VISIBLE, personIcon.getVisibility()); + // Has notification content. + statusContent = (TextView) largeResult.findViewById(R.id.text_content); + assertEquals(View.VISIBLE, statusContent.getVisibility()); + assertEquals(statusContent.getText(), MISSED_CALL); + assertThat(statusContent.getMaxLines()).isEqualTo(3); + } + + @Test + public void testCreateRemoteViewsWithNotificationTemplate() { + PeopleSpaceTile tileWithStatusAndNotification = PERSON_TILE.toBuilder() + .setNotificationDataUri(null) + .setStatuses(Arrays.asList(GAME_STATUS, + NEW_STORY_WITH_AVAILABILITY)).build(); + RemoteViews views = new PeopleTileViewHelper(mContext, + tileWithStatusAndNotification, 0, mOptions).getViews(); + View result = views.apply(mContext, null); + + TextView name = (TextView) result.findViewById(R.id.name); + assertEquals(name.getText(), NAME); + assertEquals(View.GONE, result.findViewById(R.id.subtext).getVisibility()); + assertEquals(View.GONE, result.findViewById(R.id.predefined_icon).getVisibility()); + // Has availability. + assertEquals(View.VISIBLE, result.findViewById(R.id.availability).getVisibility()); + // Has person icon. + assertEquals(View.VISIBLE, result.findViewById(R.id.person_icon).getVisibility()); + // Has notification content. + TextView statusContent = (TextView) result.findViewById(R.id.text_content); + assertEquals(View.VISIBLE, statusContent.getVisibility()); + assertEquals(statusContent.getText(), NOTIFICATION_CONTENT); + assertThat(statusContent.getMaxLines()).isEqualTo(3); + + mOptions.putInt(OPTION_APPWIDGET_MIN_WIDTH, + getSizeInDp(R.dimen.required_width_for_medium) - 1); + RemoteViews smallView = new PeopleTileViewHelper(mContext, + tileWithStatusAndNotification, 0, mOptions).getViews(); + View smallResult = smallView.apply(mContext, null); + + // Show icon instead of name. + assertEquals(View.GONE, smallResult.findViewById(R.id.name).getVisibility()); + assertEquals(View.VISIBLE, + smallResult.findViewById(R.id.predefined_icon).getVisibility()); + // Has person icon. + assertEquals(View.VISIBLE, + smallResult.findViewById(R.id.person_icon).getVisibility()); + + mOptions.putInt(OPTION_APPWIDGET_MIN_WIDTH, + getSizeInDp(R.dimen.required_width_for_large)); + mOptions.putInt(OPTION_APPWIDGET_MIN_WIDTH, + getSizeInDp(R.dimen.required_height_for_large)); + RemoteViews largeView = new PeopleTileViewHelper(mContext, + tileWithStatusAndNotification, 0, mOptions).getViews(); + View largeResult = largeView.apply(mContext, null); + + name = (TextView) largeResult.findViewById(R.id.name); + assertEquals(name.getText(), NAME); + assertEquals(View.GONE, largeResult.findViewById(R.id.subtext).getVisibility()); + assertEquals(View.GONE, largeResult.findViewById(R.id.predefined_icon).getVisibility()); + // Has availability. + assertEquals(View.VISIBLE, largeResult.findViewById(R.id.availability).getVisibility()); + // Has person icon. + View personIcon = largeResult.findViewById(R.id.person_icon); + assertEquals(View.VISIBLE, personIcon.getVisibility()); + // Has notification content. + statusContent = (TextView) largeResult.findViewById(R.id.text_content); + assertEquals(View.VISIBLE, statusContent.getVisibility()); + assertEquals(statusContent.getText(), NOTIFICATION_CONTENT); + assertThat(statusContent.getMaxLines()).isEqualTo(3); + } + + @Test + public void testGetBackgroundTextFromMessageNoPunctuation() { + String backgroundText = mPeopleTileViewHelper.getBackgroundTextFromMessage("test"); + + assertThat(backgroundText).isNull(); + } + + @Test + public void testGetBackgroundTextFromMessageSingleExclamation() { + String backgroundText = mPeopleTileViewHelper.getBackgroundTextFromMessage("test!"); + + assertThat(backgroundText).isNull(); + } + + @Test + public void testGetBackgroundTextFromMessageSingleQuestion() { + String backgroundText = mPeopleTileViewHelper.getBackgroundTextFromMessage("?test"); + + assertThat(backgroundText).isNull(); + } + + @Test + public void testGetBackgroundTextFromMessageSeparatedMarks() { + String backgroundText = mPeopleTileViewHelper.getBackgroundTextFromMessage("test! right!"); + + assertThat(backgroundText).isNull(); + } + + @Test + public void testGetBackgroundTextFromMessageDoubleExclamation() { + String backgroundText = mPeopleTileViewHelper.getBackgroundTextFromMessage("!!test"); + + assertThat(backgroundText).isEqualTo("!"); + } + + @Test + public void testGetBackgroundTextFromMessageDoubleQuestion() { + String backgroundText = mPeopleTileViewHelper.getBackgroundTextFromMessage("test??"); + + assertThat(backgroundText).isEqualTo("?"); + } + + @Test + public void testGetBackgroundTextFromMessageMixed() { + String backgroundText = mPeopleTileViewHelper.getBackgroundTextFromMessage("test?!"); + + assertThat(backgroundText).isEqualTo("!?"); + } + + @Test + public void testGetBackgroundTextFromMessageMixedInTheMiddle() { + String backgroundText = mPeopleTileViewHelper.getBackgroundTextFromMessage( + "test!? in the middle"); + + assertThat(backgroundText).isEqualTo("!?"); + } + + @Test + public void testGetBackgroundTextFromMessageMixedDifferentOrder() { + String backgroundText = mPeopleTileViewHelper.getBackgroundTextFromMessage( + "test!? in the middle"); + + assertThat(backgroundText).isEqualTo("!?"); + } + + @Test + public void testGetBackgroundTextFromMessageMultiple() { + String backgroundText = mPeopleTileViewHelper.getBackgroundTextFromMessage( + "test!?!!? in the middle"); + + assertThat(backgroundText).isEqualTo("!?"); + } + + @Test + public void testGetBackgroundTextFromMessageQuestionFirst() { + String backgroundText = mPeopleTileViewHelper.getBackgroundTextFromMessage( + "test?? in the middle!!"); + + assertThat(backgroundText).isEqualTo("?"); + } + + @Test + public void testGetBackgroundTextFromMessageExclamationFirst() { + String backgroundText = mPeopleTileViewHelper.getBackgroundTextFromMessage( + "test!! in the middle??"); + + assertThat(backgroundText).isEqualTo("!"); + } + + private int getSizeInDp(int dimenResourceId) { + return (int) (mContext.getResources().getDimension(dimenResourceId) + / mContext.getResources().getDisplayMetrics().density); + } +}