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
This commit is contained in:
Dan Sandler
2020-02-19 13:18:24 -05:00
parent 9577cd78ff
commit b11f2deb89
5 changed files with 174 additions and 59 deletions

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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() {

View File

@@ -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));

View File

@@ -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