Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f0399670 authored by Automerger Merge Worker's avatar Automerger Merge Worker
Browse files

Merge "Add importance indicator to conversation icons." into rvc-dev am: 06907ad5 am: f9833f1d

Change-Id: I9ab6b60cfddd4980b101e17e0543511119759fdf
parents 97140e6f f9833f1d
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -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>
+145 −38
Original line number Diff line number Diff line
@@ -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());
    public Drawable getConversationDrawable(ShortcutInfo info, String packageName, int uid,
            boolean important) {
        return getConversationDrawable(getBaseIconDrawable(info), packageName, uid, important);
    }

        Canvas c = new Canvas();
        ShadowGenerator shadowGenerator = new ShadowGenerator(getBadgeSize());
        c.setBitmap(bitmap);
        shadowGenerator.recreateIcon(Bitmap.createBitmap(bitmap), c);
        return createIconBitmap(bitmap);
    /**
     * 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);
    }

    /**
     * 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(ShortcutInfo info, String packageName, int uid) {
        return getConversationBitmap(getConversationDrawable(info), packageName, uid);
    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);
        }

        /**
     * Returns a {@link BitmapInfo} for the entire conversation icon including the badge.
         * Show or hide the importance ring.
         */
    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 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;
        }
    }
}
+6 −2
Original line number Diff line number Diff line
@@ -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() {
+1 −1
Original line number Diff line number Diff line
@@ -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));

+7 −6
Original line number Diff line number Diff line
@@ -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