Merge "Add importance indicator to conversation icons." into rvc-dev am: 06907ad5ee am: f9833f1d61
Change-Id: I9ab6b60cfddd4980b101e17e0543511119759fdf
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user