Merge "Add app badging to conversation icons"
This commit is contained in:
committed by
Android (Google) Code Review
commit
80288de910
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<ShortcutInfo> 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();
|
||||
|
||||
Reference in New Issue
Block a user