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) { + RemoteViews views = new RemoteViews( + context.getPackageName(), R.layout.people_space_small_avatar_tile); + CharSequence statusText = status.getDescription(); + if (TextUtils.isEmpty(statusText)) { + statusText = getStatusTextByType(context, status.getActivity()); + } + views.setTextViewText(R.id.status, 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 + 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_MEDIA: + return context.getString(R.string.video_status); + case ACTIVITY_GAME: + return context.getString(R.string.game_status); + default: + return EMPTY_STRING; } - return setCommonRemoteViewsFields(context, views, tile, appWidgetId); } private static RemoteViews setCommonRemoteViewsFields(Context context, RemoteViews views, - PeopleSpaceTile tile, int appWidgetId) { + PeopleSpaceTile tile) { 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); + if (hasNewStory) { + views.setViewVisibility(R.id.person_icon_with_story, View.VISIBLE); + views.setViewVisibility(R.id.person_icon_only, View.GONE); + views.setImageViewIcon(R.id.person_icon_inside_ring, tile.getUserIcon()); + } else { + views.setViewVisibility(R.id.person_icon_with_story, View.GONE); + views.setViewVisibility(R.id.person_icon_only, View.VISIBLE); + views.setImageViewIcon(R.id.person_icon_only, tile.getUserIcon()); + } + views.setTextViewText(R.id.name, tile.getUserName().toString()); views.setImageViewIcon(R.id.person_icon, tile.getUserIcon()); views.setBoolean(R.id.content_background, "setClipToOutline", true); @@ -466,7 +598,16 @@ public class PeopleSpaceUtils { tile.getPackageName()) ) ); + 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 @@ -484,48 +625,42 @@ public class PeopleSpaceUtils { PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE)); return views; } catch (Exception e) { - Log.e(TAG, "Failed to set common fields: " + e); + Log.e(TAG, "Failed to add launch intents: " + e); } - return null; + return views; } private static RemoteViews createNotificationRemoteViews(Context context, PeopleSpaceTile tile) { RemoteViews views = new RemoteViews( - context.getPackageName(), R.layout.people_space_small_avatar_tile); + context.getPackageName(), R.layout.people_space_notification_content_tile); Uri image = tile.getNotificationDataUri(); if (image != null) { - //TODO: Use NotificationInlineImageCache + // TODO: Use NotificationInlineImageCache views.setImageViewUri(R.id.image, image); - views.setViewVisibility(R.id.image, View.VISIBLE); + views.setViewVisibility(R.id.content_background, View.VISIBLE); + views.setBoolean(R.id.content_background, "setClipToOutline", true); views.setViewVisibility(R.id.content, View.GONE); } else { CharSequence content = tile.getNotificationContent(); views = setPunctuationRemoteViewsFields(views, content); views.setTextViewText(R.id.content, content); views.setViewVisibility(R.id.content, View.VISIBLE); - views.setViewVisibility(R.id.image, View.GONE); + views.setViewVisibility(R.id.content_background, View.GONE); } - views.setTextViewText(R.id.time, PeopleSpaceUtils.getLastInteractionString( + // TODO: Set subtext as Group Sender name once storing the name in PeopleSpaceTile. + views.setTextViewText(R.id.subtext, PeopleSpaceUtils.getLastInteractionString( context, tile.getLastInteractionTimestamp(), false)); return views; } - private static RemoteViews createStatusRemoteViews(Context context, - PeopleSpaceTile tile) { - RemoteViews views = new RemoteViews( - context.getPackageName(), R.layout.people_space_large_avatar_tile); - views.setTextViewText(R.id.status, tile.getBirthdayText()); - return views; - } - private static RemoteViews createLastInteractionRemoteViews(Context context, PeopleSpaceTile tile) { RemoteViews views = new RemoteViews( context.getPackageName(), R.layout.people_space_large_avatar_tile); String status = PeopleSpaceUtils.getLastInteractionString( context, tile.getLastInteractionTimestamp(), true); - views.setTextViewText(R.id.status, status); + views.setTextViewText(R.id.last_interaction, status); return views; } @@ -682,20 +817,35 @@ public class PeopleSpaceUtils { Duration durationSinceLastInteraction = Duration.ofMillis(now - lastInteraction); MeasureFormat formatter = MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE); + MeasureFormat shortFormatter = MeasureFormat.getInstance(Locale.getDefault(), + MeasureFormat.FormatWidth.SHORT); if (durationSinceLastInteraction.toHours() < MIN_HOUR) { - return context.getString(includeLastChatted ? R.string.last_interaction_status_less_than - : R.string.less_than_timestamp, - formatter.formatMeasures(new Measure(MIN_HOUR, MeasureUnit.HOUR))); + if (includeLastChatted) { + return context.getString(R.string.last_interaction_status_less_than, + formatter.formatMeasures(new Measure(MIN_HOUR, MeasureUnit.HOUR))); + } + return context.getString(R.string.timestamp, shortFormatter.formatMeasures( + new Measure(durationSinceLastInteraction.toMinutes(), MeasureUnit.MINUTE))); } else if (durationSinceLastInteraction.toDays() < ONE_DAY) { - return context.getString( - includeLastChatted ? R.string.last_interaction_status : R.string.timestamp, - formatter.formatMeasures( - new Measure(durationSinceLastInteraction.toHours(), MeasureUnit.HOUR))); + if (includeLastChatted) { + return context.getString(R.string.last_interaction_status, + formatter.formatMeasures( + new Measure(durationSinceLastInteraction.toHours(), + MeasureUnit.HOUR))); + } + return context.getString(R.string.timestamp, shortFormatter.formatMeasures( + new Measure(durationSinceLastInteraction.toHours(), + MeasureUnit.HOUR))); } else if (durationSinceLastInteraction.toDays() < DAYS_IN_A_WEEK) { - return context.getString( - includeLastChatted ? R.string.last_interaction_status : R.string.timestamp, - formatter.formatMeasures( - new Measure(durationSinceLastInteraction.toDays(), MeasureUnit.DAY))); + if (includeLastChatted) { + return context.getString(R.string.last_interaction_status, + formatter.formatMeasures( + new Measure(durationSinceLastInteraction.toDays(), + MeasureUnit.DAY))); + } + return context.getString(R.string.timestamp, shortFormatter.formatMeasures( + new Measure(durationSinceLastInteraction.toHours(), + MeasureUnit.DAY))); } else { return context.getString(durationSinceLastInteraction.toDays() == DAYS_IN_A_WEEK ? (includeLastChatted ? R.string.last_interaction_status : 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 4ee2759028a92..d79155cbb2fc7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/people/PeopleSpaceUtilsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/people/PeopleSpaceUtilsTest.java @@ -16,11 +16,18 @@ package com.android.systemui.people; +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 com.android.systemui.people.PeopleSpaceUtils.OPTIONS_PEOPLE_SPACE_TILE; import static com.android.systemui.people.PeopleSpaceUtils.PACKAGE_NAME; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -39,6 +46,7 @@ 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; @@ -46,13 +54,13 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.LauncherApps; +import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.pm.ShortcutInfo; import android.database.Cursor; import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Bundle; -import android.os.RemoteException; import android.os.UserHandle; import android.provider.ContactsContract; import android.provider.Settings; @@ -60,6 +68,9 @@ import android.service.notification.ConversationChannelWrapper; import android.service.notification.StatusBarNotification; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; +import android.view.View; +import android.widget.RemoteViews; +import android.widget.TextView; import com.android.internal.appwidget.IAppWidgetService; import com.android.systemui.R; @@ -101,27 +112,45 @@ 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 String NAME = "username"; private static final Person PERSON = new Person.Builder() .setName("name") .setKey("abc") .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, "username", ICON, new Intent()) + .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(); private final ShortcutInfo mShortcutInfo = new ShortcutInfo.Builder(mContext, SHORTCUT_ID_1).setLongLabel( - "name").setPerson(PERSON) + NAME).setPerson(PERSON) .build(); private final ShortcutInfo mShortcutInfoWithoutPerson = new ShortcutInfo.Builder(mContext, SHORTCUT_ID_1).setLongLabel( - "name") + NAME) .build(); private final Notification mNotification1 = new Notification.Builder(mContext, "test") .setContentTitle("TEST_TITLE") @@ -189,10 +218,12 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { @Mock private Context mMockContext; @Mock + private PackageManager mPackageManager; + @Mock private NotificationEntryManager mNotificationEntryManager; @Before - public void setUp() throws RemoteException { + public void setUp() throws Exception { MockitoAnnotations.initMocks(this); Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.PEOPLE_SPACE_CONVERSATION_TYPE, 0); @@ -212,6 +243,12 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { isNull())).thenReturn(mMockCursor); 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)); + when(mPackageManager.getApplicationIcon(anyString())).thenReturn(null); when(mNotificationEntryManager.getVisibleNotifications()) .thenReturn(List.of(mNotificationEntry1, mNotificationEntry2, mNotificationEntry3)); } @@ -621,6 +658,137 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { any()); } + @Test + public void testCreateRemoteViewsWithLastInteractionTime() { + RemoteViews views = PeopleSpaceUtils.createRemoteViews(mMockContext, + PERSON_TILE_WITHOUT_NOTIFICATION, 0); + 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()); + // No new story. + View personIcon = result.findViewById(R.id.person_icon_only); + View personIconWithStory = result.findViewById(R.id.person_icon_with_story); + assertEquals(View.VISIBLE, personIcon.getVisibility()); + assertEquals(View.GONE, personIconWithStory.getVisibility()); + // No status. + assertThat((View) result.findViewById(R.id.status)).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 = PeopleSpaceUtils.createRemoteViews(mMockContext, + tileWithAvailabilityAndNewStory, 0); + 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 new story. + View personIcon = result.findViewById(R.id.person_icon_only); + View personIconWithStory = result.findViewById(R.id.person_icon_with_story); + assertEquals(View.GONE, personIcon.getVisibility()); + assertEquals(View.VISIBLE, personIconWithStory.getVisibility()); + // No status. + assertThat((View) result.findViewById(R.id.status)).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 = PeopleSpaceUtils.createRemoteViews(mContext, + tileWithStatusTemplate, 0); + 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 new story. + View personIcon = result.findViewById(R.id.person_icon_only); + View personIconWithStory = result.findViewById(R.id.person_icon_with_story); + assertEquals(View.GONE, personIcon.getVisibility()); + assertEquals(View.VISIBLE, personIconWithStory.getVisibility()); + // Has status text from backup text. + TextView statusContent = (TextView) result.findViewById(R.id.status); + assertEquals(statusContent.getText(), mContext.getString(R.string.birthday_status)); + } + + @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); + 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 new story. + View personIcon = result.findViewById(R.id.person_icon_only); + View personIconWithStory = result.findViewById(R.id.person_icon_with_story); + assertEquals(View.GONE, personIcon.getVisibility()); + assertEquals(View.VISIBLE, personIconWithStory.getVisibility()); + // Has status. + TextView statusContent = (TextView) result.findViewById(R.id.status); + assertEquals(statusContent.getText(), GAME_DESCRIPTION); + } + + @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); + 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); + assertTrue(subtext.getText().toString().contains("weeks ago")); + // Has availability. + View availability = result.findViewById(R.id.availability); + assertEquals(View.VISIBLE, availability.getVisibility()); + // Has new story. + View personIcon = result.findViewById(R.id.person_icon_only); + View personIconWithStory = result.findViewById(R.id.person_icon_with_story); + assertEquals(View.GONE, personIcon.getVisibility()); + assertEquals(View.VISIBLE, personIconWithStory.getVisibility()); + // Has notification content. + TextView statusContent = (TextView) result.findViewById(R.id.content); + assertEquals(statusContent.getText(), NOTIFICATION_CONTENT); + } + private ConversationChannelWrapper getConversationChannelWrapper(String shortcutId, boolean importantConversation, long lastInteractionTimestamp) throws Exception { ConversationChannelWrapper convo = new ConversationChannelWrapper();