From 01c9da477e6e6b656bbc716b10f489ff5d5f54b8 Mon Sep 17 00:00:00 2001 From: Julia Reynolds Date: Fri, 14 Feb 2020 14:17:58 -0500 Subject: [PATCH] Add app badging to conversation icons Test: atest, manual Fixes: 149573346 Change-Id: I97dfdb86f00012598b836c32603c8b5057abdf00 --- .../notification/ConversationIconFactory.java | 113 ++++++++++++++++++ .../row/NotificationConversationInfo.java | 10 +- .../row/NotificationGutsManager.java | 10 +- .../row/NotificationConversationInfoTest.java | 46 +++++-- 4 files changed, 162 insertions(+), 17 deletions(-) create mode 100644 packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java b/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java new file mode 100644 index 0000000000000..885b7d396a6ca --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settingslib.notification; + +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.drawable.Drawable; +import android.os.UserHandle; +import android.util.IconDrawableFactory; + +import com.android.launcher3.icons.BaseIconFactory; +import com.android.launcher3.icons.BitmapInfo; +import com.android.launcher3.icons.ShadowGenerator; + +/** + * 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. + */ +public class ConversationIconFactory extends BaseIconFactory { + + final LauncherApps mLauncherApps; + final PackageManager mPackageManager; + final IconDrawableFactory mIconDrawableFactory; + + public ConversationIconFactory(Context context, LauncherApps la, PackageManager pm, + IconDrawableFactory iconDrawableFactory, int iconSizePx) { + super(context, context.getResources().getConfiguration().densityDpi, + iconSizePx); + mLauncherApps = la; + mPackageManager = pm; + mIconDrawableFactory = iconDrawableFactory; + } + + 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) { + return mLauncherApps.getShortcutIconDrawable(shortcutInfo, mFillResIconDpi); + } + + /** + * Get the {@link Drawable} that represents the app icon + */ + private Drawable getBadgedIcon(String packageName, int userId) { + try { + final ApplicationInfo appInfo = mPackageManager.getApplicationInfoAsUser( + packageName, PackageManager.GET_META_DATA, userId); + return mIconDrawableFactory.getBadgedIcon(appInfo, userId); + } catch (PackageManager.NameNotFoundException e) { + return mPackageManager.getDefaultActivityIcon(); + } + } + + /** + * 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 a0af4ace05b11..923c348c2359d 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 @@ -33,11 +33,9 @@ import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.content.Context; import android.content.Intent; -import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.LauncherApps; import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; import android.graphics.drawable.Icon; @@ -61,6 +59,7 @@ import android.widget.LinearLayout; import android.widget.TextView; import com.android.internal.annotations.VisibleForTesting; +import com.android.settingslib.notification.ConversationIconFactory; import com.android.settingslib.utils.ThreadUtils; import com.android.systemui.Dependency; import com.android.systemui.R; @@ -87,6 +86,7 @@ public class NotificationConversationInfo extends LinearLayout implements private PackageManager mPm; private VisualStabilityManager mVisualStabilityManager; private ShadeController mShadeController; + private ConversationIconFactory mIconFactory; private String mPackageName; private String mAppName; @@ -186,6 +186,7 @@ public class NotificationConversationInfo extends LinearLayout implements OnSettingsClickListener onSettingsClick, OnAppSettingsClickListener onAppSettingsClick, OnSnoozeClickListener onSnoozeClickListener, + ConversationIconFactory conversationIconFactory, boolean isDeviceProvisioned) { mSelectedAction = -1; mINotificationManager = iNotificationManager; @@ -203,6 +204,7 @@ public class NotificationConversationInfo extends LinearLayout implements mIsDeviceProvisioned = isDeviceProvisioned; mOnSnoozeClickListener = onSnoozeClickListener; mShadeController = Dependency.get(ShadeController.class); + mIconFactory = conversationIconFactory; mShortcutManager = shortcutManager; mLauncherApps = launcherApps; @@ -320,8 +322,8 @@ public class NotificationConversationInfo extends LinearLayout implements private void bindIcon() { ImageView image = findViewById(R.id.conversation_icon); if (mShortcutInfo != null) { - image.setImageDrawable(mLauncherApps.getShortcutBadgedIconDrawable(mShortcutInfo, - mContext.getResources().getDisplayMetrics().densityDpi)); + image.setImageBitmap(mIconFactory.getConversationBitmap( + mShortcutInfo, mPackageName, mAppUid)); } else { if (mSbn.getNotification().extras.getBoolean(EXTRA_IS_GROUP_CONVERSATION, false)) { // TODO: maybe use a generic group icon, or a composite of recent senders 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 352abcfc9214f..4915a23c5371c 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 @@ -34,6 +34,7 @@ import android.os.UserHandle; import android.provider.Settings; import android.service.notification.StatusBarNotification; import android.util.ArraySet; +import android.util.IconDrawableFactory; import android.util.Log; import android.view.HapticFeedbackConstants; import android.view.View; @@ -42,8 +43,10 @@ import android.view.accessibility.AccessibilityManager; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto; +import com.android.settingslib.notification.ConversationIconFactory; import com.android.systemui.Dependency; import com.android.systemui.Dumpable; +import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -58,7 +61,6 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider; import com.android.systemui.statusbar.notification.row.NotificationInfo.CheckSaveListener; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; -import com.android.systemui.statusbar.phone.ShadeController; import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.policy.DeviceProvisionedController; @@ -388,6 +390,10 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx notificationInfoView.closeControls(v, false); }; } + ConversationIconFactory iconFactoryLoader = new ConversationIconFactory(mContext, + launcherApps, pmUser, IconDrawableFactory.newInstance(mContext), + mContext.getResources().getDimensionPixelSize( + R.dimen.notification_guts_conversation_icon_size)); notificationInfoView.bindNotification( shortcutManager, @@ -401,8 +407,8 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx onSettingsClick, onAppSettingsClick, onSnoozeClickListener, + iconFactoryLoader, mDeviceProvisionedController.isDeviceProvisioned()); - } /** 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 e8de10f7392b1..27b263f3052b1 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 @@ -51,7 +51,8 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; -import android.graphics.drawable.Drawable; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Icon; import android.os.UserHandle; import android.service.notification.StatusBarNotification; @@ -60,12 +61,12 @@ import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.LayoutInflater; import android.view.View; -import android.widget.Button; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; import com.android.internal.logging.MetricsLogger; +import com.android.settingslib.notification.ConversationIconFactory; import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; @@ -112,7 +113,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase { private NotificationEntry mBubbleEntry; @Mock private ShortcutInfo mShortcutInfo; - private Drawable mImage; + @Mock + private Bitmap mImage; @Rule public MockitoRule mockito = MockitoJUnit.rule(); @@ -134,6 +136,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase { private NotificationGuts mNotificationGuts; @Mock private ShadeController mShadeController; + @Mock + private ConversationIconFactory mIconFactory; @Before public void setUp() throws Exception { @@ -176,9 +180,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase { when(mShortcutInfo.getShortLabel()).thenReturn("Convo name"); List shortcuts = Arrays.asList(mShortcutInfo); when(mLauncherApps.getShortcuts(any(), any())).thenReturn(shortcuts); - mImage = mContext.getDrawable(R.drawable.ic_remove); - when(mLauncherApps.getShortcutBadgedIconDrawable(eq(mShortcutInfo), - anyInt())).thenReturn(mImage); + when(mIconFactory.getConversationBitmap(any(ShortcutInfo.class), anyString(), anyInt())) + .thenReturn(mImage); mNotificationChannel = new NotificationChannel( TEST_CHANNEL, TEST_CHANNEL_NAME, IMPORTANCE_LOW); @@ -224,9 +227,10 @@ public class NotificationConversationInfoTest extends SysuiTestCase { null, null, null, + mIconFactory, true); final ImageView view = mNotificationInfo.findViewById(R.id.conversation_icon); - assertEquals(mImage, view.getDrawable()); + assertEquals(mImage, ((BitmapDrawable) view.getDrawable()).getBitmap()); } @Test @@ -244,6 +248,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { null, null, null, + mIconFactory, true); final TextView textView = mNotificationInfo.findViewById(R.id.pkg_name); assertTrue(textView.getText().toString().contains("App Name")); @@ -290,6 +295,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { null, null, null, + mIconFactory, true); final TextView textView = mNotificationInfo.findViewById(R.id.group_name); assertTrue(textView.getText().toString().contains(group.getName())); @@ -312,6 +318,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { null, null, null, + mIconFactory, true); final TextView textView = mNotificationInfo.findViewById(R.id.group_name); assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.header).getVisibility()); @@ -333,6 +340,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { null, null, null, + mIconFactory, true); final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name); assertEquals(GONE, nameView.getVisibility()); @@ -361,6 +369,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { null, null, null, + mIconFactory, true); final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name); assertEquals(VISIBLE, nameView.getVisibility()); @@ -385,6 +394,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { }, null, null, + mIconFactory, true); final View settingsButton = mNotificationInfo.findViewById(R.id.info); @@ -407,6 +417,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { null, null, null, + mIconFactory, true); final View settingsButton = mNotificationInfo.findViewById(R.id.info); assertTrue(settingsButton.getVisibility() != View.VISIBLE); @@ -430,6 +441,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { }, null, null, + mIconFactory, false); final View settingsButton = mNotificationInfo.findViewById(R.id.info); assertTrue(settingsButton.getVisibility() != View.VISIBLE); @@ -449,6 +461,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { null, null, null, + mIconFactory, true); View bubbleView = mNotificationInfo.findViewById(R.id.bubble); @@ -469,6 +482,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { null, null, null, + mIconFactory, true); View bubbleView = mNotificationInfo.findViewById(R.id.bubble); @@ -491,9 +505,9 @@ public class NotificationConversationInfoTest extends SysuiTestCase { null, null, null, + mIconFactory, true); - // Promote it mNotificationInfo.findViewById(R.id.home).performClick(); mTestableLooper.processAllMessages(); @@ -522,9 +536,9 @@ public class NotificationConversationInfoTest extends SysuiTestCase { (View v, int hours) -> { latch.countDown(); }, + mIconFactory, true); - // Promote it mNotificationInfo.findViewById(R.id.snooze).performClick(); mTestableLooper.processAllMessages(); @@ -551,6 +565,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { null, null, null, + mIconFactory, true); assertFalse(mBubbleEntry.isBubble()); @@ -583,6 +598,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { null, null, null, + mIconFactory, true); assertTrue(mBubbleEntry.isBubble()); @@ -613,6 +629,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { null, null, null, + mIconFactory, true); assertFalse(mBubbleEntry.isBubble()); @@ -641,9 +658,9 @@ public class NotificationConversationInfoTest extends SysuiTestCase { null, null, null, + mIconFactory, true); - ImageButton fave = mNotificationInfo.findViewById(R.id.fave); assertEquals(mContext.getString(R.string.notification_conversation_unfavorite), fave.getContentDescription().toString()); @@ -675,6 +692,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { null, null, null, + mIconFactory, true); ImageButton fave = mNotificationInfo.findViewById(R.id.fave); @@ -708,6 +726,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { null, null, null, + mIconFactory, true); ImageButton mute = mNotificationInfo.findViewById(R.id.mute); @@ -743,9 +762,9 @@ public class NotificationConversationInfoTest extends SysuiTestCase { null, null, null, + mIconFactory, true); - ImageButton mute = mNotificationInfo.findViewById(R.id.mute); assertEquals(mContext.getString(R.string.notification_conversation_mute), mute.getContentDescription().toString()); @@ -774,7 +793,9 @@ public class NotificationConversationInfoTest extends SysuiTestCase { null, null, null, + mIconFactory, true); + verify(mMockINotificationManager, times(1)).createConversationNotificationChannelForPackage( anyString(), anyInt(), anyString(), any(), eq(CONVERSATION_ID)); } @@ -794,7 +815,9 @@ public class NotificationConversationInfoTest extends SysuiTestCase { null, null, null, + mIconFactory, true); + verify(mMockINotificationManager, never()).createConversationNotificationChannelForPackage( anyString(), anyInt(), anyString(), any(), eq(CONVERSATION_ID)); } @@ -815,6 +838,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { null, null, null, + mIconFactory, true); mNotificationInfo.findViewById(R.id.mute).performClick();