diff options
5 files changed, 162 insertions, 47 deletions
diff --git a/packages/SettingsLib/res/values/colors.xml b/packages/SettingsLib/res/values/colors.xml index 5e8779fa289a..88c6185a53e7 100644 --- a/packages/SettingsLib/res/values/colors.xml +++ b/packages/SettingsLib/res/values/colors.xml @@ -39,4 +39,7 @@ <color name="dark_mode_icon_color_single_tone">#99000000</color> <color name="light_mode_icon_color_single_tone">#ffffff</color> + + <!-- Yellow 600, used for highlighting "important" conversations in settings & notifications --> + <color name="important_conversation">#f9ab00</color> </resources> diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java b/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java index 885b7d396a6c..9dc454f53834 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; } /** - * Turns a Drawable into a Bitmap + * Returns a {@link Drawable} for the entire conversation. The shortcut icon will be badged + * with the launcher icon of the app specified by packageName. */ - 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); + public Drawable getConversationDrawable(ShortcutInfo info, String packageName, int uid, + boolean important) { + return getConversationDrawable(getBaseIconDrawable(info), packageName, uid, important); } /** - * Returns a {@link BitmapInfo} for the entire conversation icon including the badge. + * Returns a {@link Drawable} for the entire conversation. The drawable will be badged + * with the launcher icon of the app specified by packageName. */ - public Bitmap getConversationBitmap(ShortcutInfo info, String packageName, int uid) { - return getConversationBitmap(getConversationDrawable(info), packageName, uid); + public Drawable getConversationDrawable(Drawable baseIcon, String packageName, int uid, + boolean important) { + return new ConversationIconDrawable(baseIcon, + getAppBadge(packageName, UserHandle.getUserId(uid)), + mIconBitmapSize, + mImportantConversationColor, + important); } /** - * Returns a {@link BitmapInfo} for the entire conversation icon including the badge. + * 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 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; + 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; + } } } 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 bab7840e57d7..a5258fdef975 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 @@ -325,8 +325,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 @@ -480,6 +481,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 c01f6c4ef0d2..d746822ddcff 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 138ea392f8ef..6c12c7697c8f 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<ShortcutInfo> 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 |