From b11f2deb890c570549edbde94bd8cd32be9e5527 Mon Sep 17 00:00:00 2001 From: Dan Sandler Date: Wed, 19 Feb 2020 13:18:24 -0500 Subject: [PATCH] Add importance indicator to conversation icons. We're also drawing these icons a little differently than the launcher does, both to accommodate the importance marker, as well as remove the shadows that are useful for separation vs. user wallpaper but are not appropriate for the notification surface or settings screens. Bug: 149747760 Test: make RunSettingsLibRoboTests test: atest NotificationConversationInfoTest Test: manual in notifications UI Change-Id: I3f73c025b1d32194e3d5147d03cb781e370f2a1b --- packages/SettingsLib/res/values/colors.xml | 3 + .../notification/ConversationIconFactory.java | 207 +++++++++++++----- .../row/NotificationConversationInfo.java | 8 +- .../row/NotificationGutsManager.java | 2 +- .../row/NotificationConversationInfoTest.java | 13 +- 5 files changed, 174 insertions(+), 59 deletions(-) diff --git a/packages/SettingsLib/res/values/colors.xml b/packages/SettingsLib/res/values/colors.xml index 5e8779fa289ae..88c6185a53e7d 100644 --- a/packages/SettingsLib/res/values/colors.xml +++ b/packages/SettingsLib/res/values/colors.xml @@ -39,4 +39,7 @@ #99000000 #ffffff + + + #f9ab00 diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java b/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java index 885b7d396a6ca..9dc454f538342 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java @@ -15,32 +15,48 @@ */ package com.android.settingslib.notification; +import android.annotation.ColorInt; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.LauncherApps; import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; -import android.graphics.Bitmap; import android.graphics.Canvas; -import android.graphics.drawable.BitmapDrawable; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.UserHandle; import android.util.IconDrawableFactory; +import android.util.Log; import com.android.launcher3.icons.BaseIconFactory; -import com.android.launcher3.icons.BitmapInfo; -import com.android.launcher3.icons.ShadowGenerator; +import com.android.settingslib.R; /** * Factory for creating normalized conversation icons. * We are not using Launcher's IconFactory because conversation rendering only runs on the UI - * thread, so there is no need to manage a pool across multiple threads. + * thread, so there is no need to manage a pool across multiple threads. Launcher's rendering + * also includes shadows, which are only appropriate on top of wallpaper, not embedded in UI. */ public class ConversationIconFactory extends BaseIconFactory { + // Geometry of the various parts of the design. All values are 1dp on a 48x48dp icon grid. + // Space is left around the "head" (main avatar) for + // ........ + // .HHHHHH. + // .HHHrrrr + // .HHHrBBr + // ....rrrr + + private static final float BASE_ICON_SIZE = 48f; + private static final float RING_STROKE_WIDTH = 2f; + private static final float HEAD_SIZE = BASE_ICON_SIZE - RING_STROKE_WIDTH * 2 - 2; // 40 + private static final float BADGE_SIZE = HEAD_SIZE * 0.4f; // 16 final LauncherApps mLauncherApps; final PackageManager mPackageManager; final IconDrawableFactory mIconDrawableFactory; + private int mImportantConversationColor; public ConversationIconFactory(Context context, LauncherApps la, PackageManager pm, IconDrawableFactory iconDrawableFactory, int iconSizePx) { @@ -49,65 +65,156 @@ public class ConversationIconFactory extends BaseIconFactory { mLauncherApps = la; mPackageManager = pm; mIconDrawableFactory = iconDrawableFactory; + mImportantConversationColor = context.getResources().getColor( + R.color.important_conversation, null); } - private int getBadgeSize() { - return mContext.getResources().getDimensionPixelSize( - com.android.launcher3.icons.R.dimen.profile_badge_size); - } /** * Returns the conversation info drawable */ - private Drawable getConversationDrawable(ShortcutInfo shortcutInfo) { + private Drawable getBaseIconDrawable(ShortcutInfo shortcutInfo) { return mLauncherApps.getShortcutIconDrawable(shortcutInfo, mFillResIconDpi); } /** - * Get the {@link Drawable} that represents the app icon + * Get the {@link Drawable} that represents the app icon, badged with the work profile icon + * if appropriate. */ - private Drawable getBadgedIcon(String packageName, int userId) { + private Drawable getAppBadge(String packageName, int userId) { + Drawable badge = null; try { final ApplicationInfo appInfo = mPackageManager.getApplicationInfoAsUser( packageName, PackageManager.GET_META_DATA, userId); - return mIconDrawableFactory.getBadgedIcon(appInfo, userId); + badge = mIconDrawableFactory.getBadgedIcon(appInfo, userId); } catch (PackageManager.NameNotFoundException e) { - return mPackageManager.getDefaultActivityIcon(); + 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 getConversationDrawable(ShortcutInfo info, String packageName, int uid, + boolean important) { + return getConversationDrawable(getBaseIconDrawable(info), packageName, uid, important); + } + + /** + * Returns a {@link Drawable} for the entire conversation. The drawable will be badged + * with the launcher icon of the app specified by packageName. + */ + public Drawable getConversationDrawable(Drawable baseIcon, String packageName, int uid, + boolean important) { + return new ConversationIconDrawable(baseIcon, + getAppBadge(packageName, UserHandle.getUserId(uid)), + mIconBitmapSize, + mImportantConversationColor, + important); + } + + /** + * Custom Drawable that overlays a badge drawable (e.g. notification small icon or app icon) on + * a base icon (conversation/person avatar), plus decorations indicating conversation + * importance. + */ + public static class ConversationIconDrawable extends Drawable { + private Drawable mBaseIcon; + private Drawable mBadgeIcon; + private int mIconSize; + private Paint mRingPaint; + private boolean mShowRing; + + public ConversationIconDrawable(Drawable baseIcon, + Drawable badgeIcon, + int iconSize, + @ColorInt int ringColor, + boolean showImportanceRing) { + mBaseIcon = baseIcon; + mBadgeIcon = badgeIcon; + mIconSize = iconSize; + mShowRing = showImportanceRing; + mRingPaint = new Paint(); + mRingPaint.setStyle(Paint.Style.STROKE); + mRingPaint.setColor(ringColor); + } + + /** + * Show or hide the importance ring. + */ + public void setImportant(boolean important) { + if (important != mShowRing) { + mShowRing = important; + invalidateSelf(); + } + } + + @Override + public int getIntrinsicWidth() { + return mIconSize; + } + + @Override + public int getIntrinsicHeight() { + return mIconSize; + } + + // Similar to badgeWithDrawable, but relying on the bounds of each underlying drawable + @Override + public void draw(Canvas canvas) { + final Rect bounds = getBounds(); + + // scale to our internal 48x48 grid + final float scale = bounds.width() / BASE_ICON_SIZE; + final int centerX = bounds.centerX(); + final int centerY = bounds.centerX(); + final int ringStrokeWidth = (int) (RING_STROKE_WIDTH * scale); + final int headSize = (int) (HEAD_SIZE * scale); + final int badgeSize = (int) (BADGE_SIZE * scale); + + if (mBaseIcon != null) { + mBaseIcon.setBounds( + centerX - headSize / 2, + centerY - headSize / 2, + centerX + headSize / 2, + centerY + headSize / 2); + mBaseIcon.draw(canvas); + } else { + Log.w("ConversationIconFactory", "ConversationIconDrawable has null base icon"); + } + if (mBadgeIcon != null) { + mBadgeIcon.setBounds( + bounds.right - badgeSize - ringStrokeWidth, + bounds.bottom - badgeSize - ringStrokeWidth, + bounds.right - ringStrokeWidth, + bounds.bottom - ringStrokeWidth); + mBadgeIcon.draw(canvas); + } else { + Log.w("ConversationIconFactory", "ConversationIconDrawable has null badge icon"); + } + if (mShowRing) { + mRingPaint.setStrokeWidth(ringStrokeWidth); + final float radius = badgeSize * 0.5f + ringStrokeWidth * 0.5f; // stroke outside + final float cx = bounds.right - badgeSize * 0.5f - ringStrokeWidth; + final float cy = bounds.bottom - badgeSize * 0.5f - ringStrokeWidth; + canvas.drawCircle(cx, cy, radius, mRingPaint); + } + } + + @Override + public void setAlpha(int alpha) { + // unimplemented + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + // unimplemented + } + + @Override + public int getOpacity() { + return 0; } } - - /** - * Turns a Drawable into a Bitmap - */ - BitmapInfo toBitmap(Drawable userBadgedAppIcon) { - Bitmap bitmap = createIconBitmap( - userBadgedAppIcon, 1f, getBadgeSize()); - - Canvas c = new Canvas(); - ShadowGenerator shadowGenerator = new ShadowGenerator(getBadgeSize()); - c.setBitmap(bitmap); - shadowGenerator.recreateIcon(Bitmap.createBitmap(bitmap), c); - return createIconBitmap(bitmap); - } - - /** - * Returns a {@link BitmapInfo} for the entire conversation icon including the badge. - */ - public Bitmap getConversationBitmap(ShortcutInfo info, String packageName, int uid) { - return getConversationBitmap(getConversationDrawable(info), packageName, uid); - } - - /** - * Returns a {@link BitmapInfo} for the entire conversation icon including the badge. - */ - public Bitmap getConversationBitmap(Drawable baseIcon, String packageName, int uid) { - int userId = UserHandle.getUserId(uid); - Drawable badge = getBadgedIcon(packageName, userId); - BitmapInfo iconInfo = createBadgedIconBitmap(baseIcon, - UserHandle.of(userId), - true /* shrinkNonAdaptiveIcons */); - - badgeWithDrawable(iconInfo.icon, - new BitmapDrawable(mContext.getResources(), toBitmap(badge).icon)); - return iconInfo.icon; - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java index 60eda06dd3f93..03436cd202758 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java @@ -324,8 +324,9 @@ public class NotificationConversationInfo extends LinearLayout implements private void bindIcon() { ImageView image = findViewById(R.id.conversation_icon); if (mShortcutInfo != null) { - image.setImageBitmap(mIconFactory.getConversationBitmap( - mShortcutInfo, mPackageName, mAppUid)); + image.setImageDrawable(mIconFactory.getConversationDrawable( + mShortcutInfo, mPackageName, mAppUid, + mNotificationChannel.isImportantConversation())); } else { if (mSbn.getNotification().extras.getBoolean(EXTRA_IS_GROUP_CONVERSATION, false)) { // TODO: maybe use a generic group icon, or a composite of recent senders @@ -479,6 +480,9 @@ public class NotificationConversationInfo extends LinearLayout implements mContext.getString(R.string.notification_conversation_mute)); mute.setImageResource(R.drawable.ic_notifications_silence); } + + // update icon in case importance has changed + bindIcon(); } private void updateChannel() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java index c01f6c4ef0d26..d746822ddcff1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java @@ -390,7 +390,7 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx }; } ConversationIconFactory iconFactoryLoader = new ConversationIconFactory(mContext, - launcherApps, pmUser, IconDrawableFactory.newInstance(mContext), + launcherApps, pmUser, IconDrawableFactory.newInstance(mContext, false), mContext.getResources().getDimensionPixelSize( R.dimen.notification_guts_conversation_icon_size)); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java index 138ea392f8efa..6c12c7697c8f0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java @@ -30,6 +30,7 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyString; @@ -53,8 +54,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.os.UserHandle; import android.provider.Settings; @@ -117,7 +117,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { @Mock private ShortcutInfo mShortcutInfo; @Mock - private Bitmap mImage; + private Drawable mIconDrawable; @Rule public MockitoRule mockito = MockitoJUnit.rule(); @@ -183,8 +183,9 @@ public class NotificationConversationInfoTest extends SysuiTestCase { when(mShortcutInfo.getShortLabel()).thenReturn("Convo name"); List shortcuts = Arrays.asList(mShortcutInfo); when(mLauncherApps.getShortcuts(any(), any())).thenReturn(shortcuts); - when(mIconFactory.getConversationBitmap(any(ShortcutInfo.class), anyString(), anyInt())) - .thenReturn(mImage); + when(mIconFactory.getConversationDrawable( + any(ShortcutInfo.class), anyString(), anyInt(), anyBoolean())) + .thenReturn(mIconDrawable); mNotificationChannel = new NotificationChannel( TEST_CHANNEL, TEST_CHANNEL_NAME, IMPORTANCE_LOW); @@ -233,7 +234,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { mIconFactory, true); final ImageView view = mNotificationInfo.findViewById(R.id.conversation_icon); - assertEquals(mImage, ((BitmapDrawable) view.getDrawable()).getBitmap()); + assertEquals(mIconDrawable, view.getDrawable()); } @Test