From c531f63f8cf8f460961b35204454bf2acb3bb895 Mon Sep 17 00:00:00 2001 From: Anna Zappone Date: Thu, 4 Mar 2021 21:22:13 +0000 Subject: [PATCH] Add layout for 1x1 size, update 2x1 This allows users a smaller width resize for the People Tile widget, and adjust the view accordingly after resize. Implements 1x1 and 2x1 mocks, with polish remaining for 2x1. Also fixes a crash when launching the conversation picker and bug on removing missed call notifications. Bug: 182377480 Test: PeopleSpaceUtilsTest Change-Id: Ie3b16fb3e348f8edd7bb314c9f43408dca7b43e2 --- packages/SystemUI/res/drawable/ic_message.xml | 24 ++ .../SystemUI/res/drawable/ic_photo_camera.xml | 28 +++ .../layout/people_space_large_avatar_tile.xml | 37 +-- ...people_space_notification_content_tile.xml | 154 ------------- .../layout/people_space_small_avatar_tile.xml | 190 ++++++---------- .../res/layout/people_space_small_view.xml | 59 +++++ .../res/xml/people_space_widget_info.xml | 9 +- .../systemui/people/PeopleProvider.java | 6 +- .../systemui/people/PeopleSpaceUtils.java | 210 ++++++++++++----- .../people/PeopleStoryIconFactory.java | 215 ++++++++++++++++++ .../people/widget/AppWidgetOptionsHelper.java | 7 +- .../widget/PeopleSpaceWidgetManager.java | 8 +- .../systemui/people/PeopleSpaceUtilsTest.java | 164 +++++++++---- .../widget/PeopleSpaceWidgetManagerTest.java | 8 - 14 files changed, 697 insertions(+), 422 deletions(-) create mode 100644 packages/SystemUI/res/drawable/ic_message.xml create mode 100644 packages/SystemUI/res/drawable/ic_photo_camera.xml delete mode 100644 packages/SystemUI/res/layout/people_space_notification_content_tile.xml create mode 100644 packages/SystemUI/res/layout/people_space_small_view.xml create mode 100644 packages/SystemUI/src/com/android/systemui/people/PeopleStoryIconFactory.java diff --git a/packages/SystemUI/res/drawable/ic_message.xml b/packages/SystemUI/res/drawable/ic_message.xml new file mode 100644 index 0000000000000..8219eeef4be98 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_message.xml @@ -0,0 +1,24 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_photo_camera.xml b/packages/SystemUI/res/drawable/ic_photo_camera.xml new file mode 100644 index 0000000000000..63cd4e2e5cc4b --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_photo_camera.xml @@ -0,0 +1,28 @@ + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/layout/people_space_large_avatar_tile.xml b/packages/SystemUI/res/layout/people_space_large_avatar_tile.xml index b1c13287d2032..b1a37bf71beb5 100644 --- a/packages/SystemUI/res/layout/people_space_large_avatar_tile.xml +++ b/packages/SystemUI/res/layout/people_space_large_avatar_tile.xml @@ -28,48 +28,27 @@ - - - - - + android:id="@+id/availability" + android:layout_width="10dp" + android:layout_height="10dp" + android:background="@drawable/circle_green_10dp"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/SystemUI/res/layout/people_space_small_avatar_tile.xml b/packages/SystemUI/res/layout/people_space_small_avatar_tile.xml index 33004952d9470..6a606e10e3fb3 100644 --- a/packages/SystemUI/res/layout/people_space_small_avatar_tile.xml +++ b/packages/SystemUI/res/layout/people_space_small_avatar_tile.xml @@ -1,44 +1,69 @@ + ~ Copyright (C) 2020 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. + --> - - + android:layout_height="wrap_content"> + + + + + + android:layout_width="match_parent" + android:layout_height="match_parent"> - - - - - - - - - - + - - + + - - \ No newline at end of file + + + diff --git a/packages/SystemUI/res/layout/people_space_small_view.xml b/packages/SystemUI/res/layout/people_space_small_view.xml new file mode 100644 index 0000000000000..7b1abaed28ca9 --- /dev/null +++ b/packages/SystemUI/res/layout/people_space_small_view.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/xml/people_space_widget_info.xml b/packages/SystemUI/res/xml/people_space_widget_info.xml index 35188d8e11ad6..d2bff180ef8ab 100644 --- a/packages/SystemUI/res/xml/people_space_widget_info.xml +++ b/packages/SystemUI/res/xml/people_space_widget_info.xml @@ -15,10 +15,11 @@ --> entry.getRanking() != null && entry.getRanking().getConversationShortcutInfo() != null) - .collect(Collectors.toMap(PeopleTileKey::new, e -> e)); + .collect(Collectors.toMap(PeopleTileKey::new, e -> e, + // Handle duplicate keys to avoid crashes. + (e1, e2) -> e1.getSbn().getNotification().when + > e2.getSbn().getNotification().when ? e1 : e2)); if (DEBUG) { Log.d(TAG, "Number of visible notifications:" + visibleNotifications.size()); } @@ -401,9 +419,11 @@ public class PeopleSpaceUtils { /** Creates a {@link RemoteViews} for {@code tile}. */ public static RemoteViews createRemoteViews(Context context, - PeopleSpaceTile tile, int appWidgetId) { - RemoteViews viewsForTile = getViewForTile(context, tile); - RemoteViews views = setCommonRemoteViewsFields(context, viewsForTile, tile); + 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); } @@ -411,15 +431,16 @@ public class PeopleSpaceUtils { * 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) { + 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); + return createMissedCallRemoteViews(context, tile, layoutSize); } if (tile.getNotificationKey() != null) { if (DEBUG) Log.d(TAG, "Create notification view"); - return createNotificationRemoteViews(context, tile); + return createNotificationRemoteViews(context, tile, layoutSize); } // TODO: Add sorting when we expose timestamp of statuses. @@ -429,7 +450,7 @@ public class PeopleSpaceUtils { ConversationStatus birthdayStatus = getBirthdayStatus(tile, statusesForEntireView); if (birthdayStatus != null) { if (DEBUG) Log.d(TAG, "Create birthday view"); - return createStatusRemoteViews(context, birthdayStatus); + return createStatusRemoteViews(context, birthdayStatus, layoutSize); } if (!statusesForEntireView.isEmpty()) { @@ -437,10 +458,48 @@ public class PeopleSpaceUtils { Log.d(TAG, "Create status view for: " + statusesForEntireView.get(0).getActivity()); } - return createStatusRemoteViews(context, statusesForEntireView.get(0)); + return createStatusRemoteViews(context, statusesForEntireView.get(0), layoutSize); } - return createLastInteractionRemoteViews(context, tile); + 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 @@ -478,14 +537,22 @@ public class PeopleSpaceUtils { } } - private static RemoteViews createStatusRemoteViews(Context context, ConversationStatus status) { - RemoteViews views = new RemoteViews( - context.getPackageName(), R.layout.people_space_small_avatar_tile); + 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.setTextViewText(R.id.status, statusText); + 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); @@ -494,6 +561,8 @@ public class PeopleSpaceUtils { 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; } @@ -519,7 +588,7 @@ public class PeopleSpaceUtils { } private static RemoteViews setCommonRemoteViewsFields(Context context, RemoteViews views, - PeopleSpaceTile tile) { + PeopleSpaceTile tile, int maxAvatarSize) { try { boolean isAvailable = tile.getStatuses() != null && tile.getStatuses().stream().anyMatch( @@ -532,27 +601,20 @@ public class PeopleSpaceUtils { 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); + 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); - views.setImageViewBitmap( - R.id.package_icon, - PeopleSpaceUtils.convertDrawableToBitmap( - context.getPackageManager().getApplicationIcon( - tile.getPackageName()) - ) - ); return views; } catch (Exception e) { Log.e(TAG, "Failed to set common fields: " + e); @@ -585,47 +647,77 @@ public class PeopleSpaceUtils { } catch (Exception e) { Log.e(TAG, "Failed to add launch intents: " + e); } + return views; } private static RemoteViews createMissedCallRemoteViews(Context context, - PeopleSpaceTile tile) { - RemoteViews views = new RemoteViews( - context.getPackageName(), R.layout.people_space_small_avatar_tile); - views.setTextViewText(R.id.status, tile.getNotificationContent()); - views.setImageViewResource(R.id.status_defined_icon, R.drawable.ic_phone_missed); + 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) { - RemoteViews views = new RemoteViews( - context.getPackageName(), R.layout.people_space_notification_content_tile); + 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.content, View.GONE); + 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.content, content); - views.setViewVisibility(R.id.content, View.VISIBLE); + 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.setTextViewText(R.id.subtext, PeopleSpaceUtils.getLastInteractionString( - context, tile.getLastInteractionTimestamp())); + views.setViewVisibility(R.id.subtext, View.GONE); return views; } private static RemoteViews createLastInteractionRemoteViews(Context context, - PeopleSpaceTile tile) { - RemoteViews views = new RemoteViews( - context.getPackageName(), R.layout.people_space_large_avatar_tile); + 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); @@ -900,18 +992,22 @@ public class PeopleSpaceUtils { removeBirthdayStatusIfPresent(appWidgetManager, context, storedTile, appWidgetId); } - /** Updates tile in app widget options and the current view. */ - public static void updateAppWidgetOptionsAndView(AppWidgetManager appWidgetManager, - Context context, int appWidgetId, PeopleSpaceTile tile) { - AppWidgetOptionsHelper.setPeopleTile(appWidgetManager, appWidgetId, tile); - + private 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); + RemoteViews views = createRemoteViews(context, tile, appWidgetId, options); // Tell the AppWidgetManager to perform an update on the current app widget. appWidgetManager.updateAppWidget(appWidgetId, views); } + /** Updates tile in app widget options and the current view. */ + public static void updateAppWidgetOptionsAndView(AppWidgetManager appWidgetManager, + Context context, int appWidgetId, PeopleSpaceTile tile) { + Bundle options = AppWidgetOptionsHelper.setPeopleTile(appWidgetManager, appWidgetId, tile); + updateAppWidgetViews(appWidgetManager, context, appWidgetId, tile, options); + } + /** * Returns lookup keys for all contacts with a birthday today. * diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleStoryIconFactory.java b/packages/SystemUI/src/com/android/systemui/people/PeopleStoryIconFactory.java new file mode 100644 index 0000000000000..145fee5e762af --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/people/PeopleStoryIconFactory.java @@ -0,0 +1,215 @@ +/* + * 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 android.annotation.ColorInt; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.IconDrawableFactory; +import android.util.Log; +import android.util.TypedValue; + +import com.android.launcher3.icons.BaseIconFactory; +import com.android.systemui.R; + +class PeopleStoryIconFactory extends BaseIconFactory { + + private static final int PADDING = 2; + private static final int RING_WIDTH = 2; + private static final int MAX_BADGE_SIZE = 36; + + final PackageManager mPackageManager; + final IconDrawableFactory mIconDrawableFactory; + private int mImportantConversationColor; + private int mAccentColor; + private float mDensity; + private float mIconSize; + + PeopleStoryIconFactory(Context context, PackageManager pm, + IconDrawableFactory iconDrawableFactory, int iconSizeDp) { + super(context, context.getResources().getConfiguration().densityDpi, + (int) (iconSizeDp * context.getResources().getDisplayMetrics().density)); + mDensity = context.getResources().getDisplayMetrics().density; + mIconSize = mDensity * iconSizeDp; + mPackageManager = pm; + mIconDrawableFactory = iconDrawableFactory; + mImportantConversationColor = context.getColor(R.color.important_conversation); + TypedValue typedValue = new TypedValue(); + context.getTheme().resolveAttribute(android.R.attr.colorAccent, typedValue, true); + mAccentColor = context.getColor(typedValue.resourceId); + } + + + /** + * Gets the {@link Drawable} that represents the app icon, badged with the work profile icon + * if appropriate. + */ + private Drawable getAppBadge(String packageName, int userId) { + Drawable badge = null; + try { + final ApplicationInfo appInfo = mPackageManager.getApplicationInfoAsUser( + packageName, PackageManager.GET_META_DATA, userId); + badge = mIconDrawableFactory.getBadgedIcon(appInfo, userId); + } catch (PackageManager.NameNotFoundException e) { + badge = mPackageManager.getDefaultActivityIcon(); + } + return badge; + } + + /** + * Returns a {@link Drawable} for the entire conversation. The shortcut icon will be badged + * with the launcher icon of the app specified by packageName. + */ + public Drawable getPeopleTileDrawable(Drawable headDrawable, String packageName, int userId, + boolean important, boolean newStory) { + return new PeopleStoryIconDrawable(headDrawable, getAppBadge(packageName, userId), + mIconBitmapSize, mImportantConversationColor, important, mIconSize, mDensity, + mAccentColor, newStory); + } + + /** + * Custom drawable which overlays a badge drawable on a head icon (conversation/person avatar), + * with decorations indicating Important conversations and having a New Story. + */ + public static class PeopleStoryIconDrawable extends Drawable { + private float mFullIconSize; + private Drawable mAvatar; + private Drawable mBadgeIcon; + private int mIconSize; + private Paint mPriorityRingPaint; + private boolean mShowImportantRing; + private boolean mShowStoryRing; + private Paint mStoryPaint; + private float mDensity; + + PeopleStoryIconDrawable(Drawable avatar, + Drawable badgeIcon, + int iconSize, + @ColorInt int ringColor, + boolean showImportantRing, float fullIconSize, float density, + @ColorInt int accentColor, boolean showStoryRing) { + mAvatar = avatar; + mBadgeIcon = badgeIcon; + mIconSize = iconSize; + mShowImportantRing = showImportantRing; + mPriorityRingPaint = new Paint(); + mPriorityRingPaint.setStyle(Paint.Style.FILL_AND_STROKE); + mPriorityRingPaint.setColor(ringColor); + mShowStoryRing = showStoryRing; + mStoryPaint = new Paint(); + mStoryPaint.setStyle(Paint.Style.STROKE); + mStoryPaint.setColor(accentColor); + mFullIconSize = fullIconSize; + mDensity = density; + } + + @Override + public int getIntrinsicWidth() { + return mIconSize; + } + + @Override + public int getIntrinsicHeight() { + return mIconSize; + } + + @Override + public void draw(Canvas canvas) { + final Rect bounds = getBounds(); + final int minBound = Math.min(bounds.height(), bounds.width()); + // Scale head icon and app icon to our canvas. + float scale = minBound / mFullIconSize; + + int paddingInDp = (int) (PADDING * mDensity); + int ringStrokeWidth = (int) (RING_WIDTH * mDensity); + mPriorityRingPaint.setStrokeWidth(ringStrokeWidth); + mStoryPaint.setStrokeWidth(ringStrokeWidth); + + int scaledFullIconSize = (int) (mFullIconSize * scale); + int avatarSize = scaledFullIconSize - (paddingInDp * 2); + if (mAvatar != null) { + int leftAndTopPadding = paddingInDp; + int rightAndBottomPadding = avatarSize + paddingInDp; + if (mShowStoryRing) { + int headCenter = scaledFullIconSize / 2; + canvas.drawCircle(headCenter, headCenter, + getRadius(avatarSize, ringStrokeWidth), + mStoryPaint); + leftAndTopPadding += (ringStrokeWidth + paddingInDp); + rightAndBottomPadding -= (ringStrokeWidth + paddingInDp); + } + mAvatar.setBounds(leftAndTopPadding, + leftAndTopPadding, + rightAndBottomPadding, + rightAndBottomPadding); + mAvatar.draw(canvas); + } else { + Log.w("PeopleStoryIconFactory", "Null avatar icon"); + } + + // Determine badge size from either the size relative to the head icon, or max size. + int maxBadgeSize = (int) (MAX_BADGE_SIZE * mDensity); + int badgeSizeRelativeToHead = (int) (avatarSize / 2.4); + int badgeSize = Math.min(maxBadgeSize, badgeSizeRelativeToHead); + if (mBadgeIcon != null) { + int leftAndTopPadding = scaledFullIconSize - badgeSize; + int rightAndBottomPadding = scaledFullIconSize; + if (mShowImportantRing) { + int badgeCenter = leftAndTopPadding + (badgeSize / 2); + canvas.drawCircle(badgeCenter, badgeCenter, + getRadius(badgeSize, ringStrokeWidth), + mPriorityRingPaint); + leftAndTopPadding += ringStrokeWidth; + rightAndBottomPadding -= ringStrokeWidth; + } + mBadgeIcon.setBounds( + leftAndTopPadding, + leftAndTopPadding, + rightAndBottomPadding, + rightAndBottomPadding); + mBadgeIcon.draw(canvas); + } else { + Log.w("PeopleStoryIconFactory", "Null badge icon"); + } + } + + private int getRadius(int circleWidth, int circleStrokeWidth) { + return (circleWidth - circleStrokeWidth) / 2; + } + + @Override + public void setAlpha(int alpha) { + // unimplemented + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + // unimplemented + } + + @Override + public int getOpacity() { + return 0; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/AppWidgetOptionsHelper.java b/packages/SystemUI/src/com/android/systemui/people/widget/AppWidgetOptionsHelper.java index df08ee4a42bf0..7254eec71d07f 100644 --- a/packages/SystemUI/src/com/android/systemui/people/widget/AppWidgetOptionsHelper.java +++ b/packages/SystemUI/src/com/android/systemui/people/widget/AppWidgetOptionsHelper.java @@ -37,15 +37,16 @@ public class AppWidgetOptionsHelper { public static final String OPTIONS_PEOPLE_TILE = "options_people_tile"; /** Sets {@link PeopleSpaceTile} in AppWidgetOptions. */ - public static void setPeopleTile(AppWidgetManager appWidgetManager, int appWidgetId, + public static Bundle setPeopleTile(AppWidgetManager appWidgetManager, int appWidgetId, PeopleSpaceTile tile) { + Bundle options = appWidgetManager.getAppWidgetOptions(appWidgetId); if (tile == null) { if (DEBUG) Log.d(TAG, "Requested to store null tile"); - return; + return options; } - Bundle options = appWidgetManager.getAppWidgetOptions(appWidgetId); options.putParcelable(OPTIONS_PEOPLE_TILE, tile); appWidgetManager.updateAppWidgetOptions(appWidgetId, options); + return options; } /** Gets {@link PeopleSpaceTile} from AppWidgetOptions. */ diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java index 7da9a80ca287c..30eb2ac160c7b 100644 --- a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java +++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java @@ -217,8 +217,7 @@ public class PeopleSpaceWidgetManager { .setUserName(info.getLabel()) .setUserIcon( PeopleSpaceTile.convertDrawableToIcon(mLauncherApps.getShortcutIconDrawable( - info, 0)) - ) + info, 0))) .setContactUri(uri) .setStatuses(conversation.getStatuses()) .setLastInteractionTimestamp(conversation.getLastEventTimestamp()) @@ -250,9 +249,12 @@ public class PeopleSpaceWidgetManager { } storedTile = storedTile .toBuilder() + // Reset notification content. .setNotificationKey(null) .setNotificationContent(null) .setNotificationDataUri(null) + // Reset missed calls category. + .setNotificationCategory(null) .build(); } updateAppWidgetOptionsAndView(mAppWidgetManager, mContext, appWidgetId, storedTile); @@ -333,6 +335,8 @@ public class PeopleSpaceWidgetManager { addNewWidget(appWidgetId, optionsKey); AppWidgetOptionsHelper.removePeopleTileKey(mAppWidgetManager, appWidgetId); } + // Update views for new widget dimensions. + updateWidgets(new int[]{appWidgetId}); } /** Adds{@code tile} mapped to {@code appWidgetId}. */ 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 3fed07472c358..410a809bd3945 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/people/PeopleSpaceUtilsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/people/PeopleSpaceUtilsTest.java @@ -21,15 +21,16 @@ 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.PeopleSpaceUtils.getPeopleTileFromPersistentStorage; 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.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -59,6 +60,8 @@ import android.content.pm.LauncherApps; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.pm.ShortcutInfo; +import android.content.res.Configuration; +import android.content.res.Resources; import android.database.Cursor; import android.graphics.drawable.Icon; import android.net.Uri; @@ -226,6 +229,8 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { @Mock private NotificationEntryManager mNotificationEntryManager; + private Bundle mOptions; + @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); @@ -233,12 +238,12 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { Settings.Global.PEOPLE_SPACE_CONVERSATION_TYPE, 0); int[] widgetIdsArray = {WIDGET_ID_WITH_SHORTCUT, WIDGET_ID_WITHOUT_SHORTCUT}; - Bundle options = new Bundle(); - options.putParcelable(OPTIONS_PEOPLE_TILE, PERSON_TILE); + mOptions = new Bundle(); + mOptions.putParcelable(OPTIONS_PEOPLE_TILE, PERSON_TILE); when(mIAppWidgetService.getAppWidgetIds(any())).thenReturn(widgetIdsArray); when(mAppWidgetManager.getAppWidgetOptions(eq(WIDGET_ID_WITH_SHORTCUT))) - .thenReturn(options); + .thenReturn(mOptions); when(mAppWidgetManager.getAppWidgetOptions(eq(WIDGET_ID_WITHOUT_SHORTCUT))) .thenReturn(new Bundle()); @@ -252,6 +257,10 @@ 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)); @@ -665,7 +674,7 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { @Test public void testCreateRemoteViewsWithLastInteractionTime() { RemoteViews views = PeopleSpaceUtils.createRemoteViews(mMockContext, - PERSON_TILE_WITHOUT_NOTIFICATION, 0); + PERSON_TILE_WITHOUT_NOTIFICATION, 0, mOptions); View result = views.apply(mContext, null); TextView name = (TextView) result.findViewById(R.id.name); @@ -676,13 +685,22 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { // 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); + // Shows person icon. + View personIcon = result.findViewById(R.id.person_icon); assertEquals(View.VISIBLE, personIcon.getVisibility()); - assertEquals(View.GONE, personIconWithStory.getVisibility()); // No status. - assertThat((View) result.findViewById(R.id.status)).isNull(); + 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 @@ -694,7 +712,7 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { PERSON_TILE_WITHOUT_NOTIFICATION.getId(), ACTIVITY_GAME).build())).build(); RemoteViews views = PeopleSpaceUtils.createRemoteViews(mMockContext, - tileWithAvailabilityAndNewStory, 0); + tileWithAvailabilityAndNewStory, 0, mOptions); View result = views.apply(mContext, null); TextView name = (TextView) result.findViewById(R.id.name); @@ -705,13 +723,23 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { // 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 person icon. + View personIcon = result.findViewById(R.id.person_icon); + assertEquals(View.VISIBLE, personIcon.getVisibility()); // No status. - assertThat((View) result.findViewById(R.id.status)).isNull(); + 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 @@ -723,7 +751,7 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { PERSON_TILE_WITHOUT_NOTIFICATION.getId(), ACTIVITY_BIRTHDAY).build())).build(); RemoteViews views = PeopleSpaceUtils.createRemoteViews(mContext, - tileWithStatusTemplate, 0); + tileWithStatusTemplate, 0, mOptions); View result = views.apply(mContext, null); TextView name = (TextView) result.findViewById(R.id.name); @@ -731,14 +759,25 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { // 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 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.status); + 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 @@ -748,7 +787,7 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { Arrays.asList(GAME_STATUS, NEW_STORY_WITH_AVAILABILITY)).build(); RemoteViews views = PeopleSpaceUtils.createRemoteViews(mContext, - tileWithStatusTemplate, 0); + tileWithStatusTemplate, 0, mOptions); View result = views.apply(mContext, null); TextView name = (TextView) result.findViewById(R.id.name); @@ -756,14 +795,25 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { // 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 person icon. + View personIcon = result.findViewById(R.id.person_icon); + assertEquals(View.VISIBLE, personIcon.getVisibility()); // Has status. - TextView statusContent = (TextView) result.findViewById(R.id.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 @@ -774,7 +824,7 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { .setNotificationContent(MISSED_CALL) .build(); RemoteViews views = PeopleSpaceUtils.createRemoteViews(mContext, - tileWithMissedCallNotification, 0); + tileWithMissedCallNotification, 0, mOptions); View result = views.apply(mContext, null); TextView name = (TextView) result.findViewById(R.id.name); @@ -782,16 +832,25 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { // Has availability. View availability = result.findViewById(R.id.availability); assertEquals(View.GONE, availability.getVisibility()); - // Has new story. - View personIcon = result.findViewById(R.id.person_icon_only); - View personIconWithStory = result.findViewById(R.id.person_icon_with_story); + // Has person icon. + View personIcon = result.findViewById(R.id.person_icon); assertEquals(View.VISIBLE, personIcon.getVisibility()); - assertEquals(View.GONE, personIconWithStory.getVisibility()); - // Has status. - TextView statusContent = (TextView) result.findViewById(R.id.status); + // 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() { @@ -800,24 +859,35 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { .setStatuses(Arrays.asList(GAME_STATUS, NEW_STORY_WITH_AVAILABILITY)).build(); RemoteViews views = PeopleSpaceUtils.createRemoteViews(mContext, - tileWithStatusAndNotification, 0); + 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); - assertTrue(subtext.getText().toString().contains("weeks ago")); + assertEquals(View.GONE, subtext.getVisibility()); // 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 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.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()); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java index aef75beb3d56e..096e51b562635 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java @@ -626,14 +626,6 @@ public class PeopleSpaceWidgetManagerTest extends SysuiTestCase { mManager.updateWidgets(widgetIdsArray); mClock.advanceTime(MIN_LINGER_DURATION); - // If we had to fetch Tile from persistent storage, we want to make sure we write it to - // options. - verify(mAppWidgetManager, times(1)) - .updateAppWidgetOptions(eq(WIDGET_ID_WITH_SHORTCUT), - mBundleArgumentCaptor.capture()); - Bundle bundle = mBundleArgumentCaptor.getValue(); - PeopleSpaceTile tile = bundle.getParcelable(OPTIONS_PEOPLE_TILE); - assertThat(tile.getId()).isEqualTo(SHORTCUT_ID); verify(mAppWidgetManager, times(1)).updateAppWidget(eq(WIDGET_ID_WITH_SHORTCUT), any()); }