Show LargeIcon as wide for apps targeting S.

* This does not inhibit the grouping behavior which demotes this image and shows it in a smaller square area on the left.
* This also converts the Notification class to calculate margins and sizes in DIP instead of PX, as that is more resilient.

Fixes: 175409684
Test: atest NotificationTemplateTest
Change-Id: I35d28c8df341dbbac2774026c6ca749e296c0482
This commit is contained in:
Jeff DeCew
2020-12-21 14:18:46 -05:00
parent a2114a56c8
commit 21147c02f8
6 changed files with 131 additions and 43 deletions

View File

@@ -30,7 +30,6 @@ import android.annotation.IdRes;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.Px;
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
@@ -218,6 +217,11 @@ public class Notification implements Parcelable
*/
private static final int MAX_REPLY_HISTORY = 5;
/**
* Maximum aspect ratio of the large icon. 16:9
*/
private static final float MAX_LARGE_ICON_ASPECT_RATIO = 16f / 9f;
/**
* Maximum number of (generic) action buttons in a notification (contextual action buttons are
* handled separately).
@@ -4229,9 +4233,9 @@ public class Notification implements Parcelable
/**
* Add a large icon to the notification content view.
*
* In the platform template, this image will be shown on the left of the notification view
* in place of the {@link #setSmallIcon(Icon) small icon} (which will be placed in a small
* badge atop the large icon).
* In the platform template, this image will be shown either on the right of the
* notification, with an aspect ratio of up to 16:9, or (when the notification is grouped)
* on the left in place of the {@link #setSmallIcon(Icon) small icon}.
*/
@NonNull
public Builder setLargeIcon(Bitmap b) {
@@ -4241,9 +4245,9 @@ public class Notification implements Parcelable
/**
* Add a large icon to the notification content view.
*
* In the platform template, this image will be shown on the left of the notification view
* in place of the {@link #setSmallIcon(Icon) small icon} (which will be placed in a small
* badge atop the large icon).
* In the platform template, this image will be shown either on the right of the
* notification, with an aspect ratio of up to 16:9, or (when the notification is grouped)
* on the left in place of the {@link #setSmallIcon(Icon) small icon}.
*/
@NonNull
public Builder setLargeIcon(Icon icon) {
@@ -5111,8 +5115,7 @@ public class Notification implements Parcelable
if (result == null) {
result = new TemplateBindResult();
}
final boolean largeIconShown = bindLargeIcon(contentView, p);
calculateLargeIconMarginEnd(largeIconShown, result);
bindLargeIcon(contentView, p, result);
if (p.mHeaderless) {
// views in the headerless (collapsed) state
result.mHeadingExtraMarginSet.applyToView(contentView,
@@ -5124,28 +5127,54 @@ public class Notification implements Parcelable
}
}
private void calculateLargeIconMarginEnd(boolean largeIconShown,
// This code is executed on behalf of other apps' notifications, sometimes even by 3p apps,
// a use case that is not supported by the Compat Framework library. Workarounds to resolve
// the change's state in NotificationManagerService were very complex. These behavior
// changes are entirely visual, and should otherwise be undetectable by apps.
@SuppressWarnings("AndroidFrameworkCompatChange")
private void calculateLargeIconDimens(boolean largeIconShown,
@NonNull TemplateBindResult result) {
final Resources resources = mContext.getResources();
final int contentMargin = resources.getDimensionPixelOffset(
R.dimen.notification_content_margin_end);
final int expanderSize = resources.getDimensionPixelSize(
R.dimen.notification_header_expand_icon_size) - contentMargin;
final int extraMarginEndIfVisible = resources.getDimensionPixelSize(
R.dimen.notification_right_icon_size) + contentMargin;
result.setRightIconState(largeIconShown, extraMarginEndIfVisible, expanderSize);
final float density = resources.getDisplayMetrics().density;
final float contentMarginDp = resources.getDimension(
R.dimen.notification_content_margin_end) / density;
final float expanderSizeDp = resources.getDimension(
R.dimen.notification_header_expand_icon_size) / density - contentMarginDp;
final float viewHeightDp = resources.getDimension(
R.dimen.notification_right_icon_size) / density;
float viewWidthDp = viewHeightDp; // icons are 1:1 by default
if (largeIconShown && (
mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.S
|| DevFlags.shouldBackportSNotifRules(mContext.getContentResolver()))) {
Drawable drawable = mN.mLargeIcon.loadDrawable(mContext);
if (drawable != null) {
int iconWidth = drawable.getIntrinsicWidth();
int iconHeight = drawable.getIntrinsicHeight();
if (iconWidth > iconHeight && iconHeight > 0) {
final float maxViewWidthDp = viewHeightDp * MAX_LARGE_ICON_ASPECT_RATIO;
viewWidthDp = Math.min(viewHeightDp * iconWidth / iconHeight,
maxViewWidthDp);
}
}
}
final float extraMarginEndDpIfVisible = viewWidthDp + contentMarginDp;
result.setRightIconState(largeIconShown, viewWidthDp,
extraMarginEndDpIfVisible, expanderSizeDp);
}
/**
* Bind the large icon.
* @return if the largeIcon is visible
*/
private boolean bindLargeIcon(RemoteViews contentView, StandardTemplateParams p) {
private void bindLargeIcon(RemoteViews contentView, @NonNull StandardTemplateParams p,
@NonNull TemplateBindResult result) {
if (mN.mLargeIcon == null && mN.largeIcon != null) {
mN.mLargeIcon = Icon.createWithBitmap(mN.largeIcon);
}
boolean showLargeIcon = mN.mLargeIcon != null && !p.hideLargeIcon;
calculateLargeIconDimens(showLargeIcon, result);
if (showLargeIcon) {
contentView.setViewLayoutWidth(R.id.right_icon,
result.mRightIconWidthDp, TypedValue.COMPLEX_UNIT_DIP);
contentView.setViewVisibility(R.id.right_icon, View.VISIBLE);
contentView.setImageViewIcon(R.id.right_icon, mN.mLargeIcon);
processLargeLegacyIcon(mN.mLargeIcon, contentView, p);
@@ -5155,7 +5184,6 @@ public class Notification implements Parcelable
// visibility) is used by NotificationGroupingUtil to set the visibility.
contentView.setImageViewIcon(R.id.right_icon, null);
}
return showLargeIcon;
}
private void bindNotificationHeader(RemoteViews contentView, StandardTemplateParams p) {
@@ -7793,7 +7821,8 @@ public class Notification implements Parcelable
// NOTE: This template doesn't support moving this icon to the left, so we don't
// need to fully apply the MarginSet
contentView.setViewLayoutMargin(R.id.notification_messaging, RemoteViews.MARGIN_END,
bindResult.mHeadingExtraMarginSet.getValue(), TypedValue.COMPLEX_UNIT_PX);
bindResult.mHeadingExtraMarginSet.getDpValue(),
TypedValue.COMPLEX_UNIT_DIP);
}
contentView.setInt(R.id.status_bar_latest_event_content, "setLayoutColor",
mBuilder.isColorized(p)
@@ -8792,7 +8821,8 @@ public class Notification implements Parcelable
// also update the end margin to account for the large icon or expander
Resources resources = mBuilder.mContext.getResources();
result.mTitleMarginSet.applyToView(remoteViews, R.id.notification_main_column,
resources.getDimensionPixelOffset(R.dimen.notification_content_margin_end));
resources.getDimension(R.dimen.notification_content_margin_end)
/ resources.getDisplayMetrics().density);
}
}
@@ -11030,6 +11060,7 @@ public class Notification implements Parcelable
*/
private static class TemplateBindResult {
boolean mRightIconVisible;
float mRightIconWidthDp;
/**
* The margin end that needs to be added to the heading so that it won't overlap
@@ -11054,11 +11085,13 @@ public class Notification implements Parcelable
*/
public final MarginSet mTitleMarginSet = new MarginSet();
public void setRightIconState(boolean visible, int marginEndIfVisible, int expanderSize) {
public void setRightIconState(boolean visible, float widthDp,
float marginEndDpIfVisible, float expanderSizeDp) {
mRightIconVisible = visible;
mHeadingExtraMarginSet.setValues(0, marginEndIfVisible);
mHeadingFullMarginSet.setValues(expanderSize, marginEndIfVisible + expanderSize);
mTitleMarginSet.setValues(0, marginEndIfVisible + expanderSize);
mRightIconWidthDp = widthDp;
mHeadingExtraMarginSet.setValues(0, marginEndDpIfVisible);
mHeadingFullMarginSet.setValues(expanderSizeDp, marginEndDpIfVisible + expanderSizeDp);
mTitleMarginSet.setValues(0, marginEndDpIfVisible + expanderSizeDp);
}
/**
@@ -11067,10 +11100,10 @@ public class Notification implements Parcelable
* left_icon and adjust the margins, and to undo that change as well.
*/
private class MarginSet {
private int mValueIfGone;
private int mValueIfVisible;
private float mValueIfGone;
private float mValueIfVisible;
public void setValues(int valueIfGone, int valueIfVisible) {
public void setValues(float valueIfGone, float valueIfVisible) {
mValueIfGone = valueIfGone;
mValueIfVisible = valueIfVisible;
}
@@ -11080,23 +11113,26 @@ public class Notification implements Parcelable
}
public void applyToView(@NonNull RemoteViews views, @IdRes int viewId,
@Px int extraMargin) {
final int marginEnd = getValue() + extraMargin;
float extraMarginDp) {
final float marginEndDp = getDpValue() + extraMarginDp;
if (viewId == R.id.notification_header) {
views.setInt(R.id.notification_header, "setTopLineExtraMarginEnd", marginEnd);
views.setFloat(R.id.notification_header,
"setTopLineExtraMarginEndDp", marginEndDp);
} else {
views.setViewLayoutMargin(viewId, RemoteViews.MARGIN_END,
marginEnd, TypedValue.COMPLEX_UNIT_PX);
marginEndDp, TypedValue.COMPLEX_UNIT_DIP);
}
if (mRightIconVisible) {
views.setIntTag(viewId, R.id.tag_margin_end_when_icon_visible,
mValueIfVisible + extraMargin);
TypedValue.createComplexDimension(
mValueIfVisible + extraMarginDp, TypedValue.COMPLEX_UNIT_DIP));
views.setIntTag(viewId, R.id.tag_margin_end_when_icon_gone,
mValueIfGone + extraMargin);
TypedValue.createComplexDimension(
mValueIfGone + extraMarginDp, TypedValue.COMPLEX_UNIT_DIP));
}
}
public int getValue() {
public float getDpValue() {
return mRightIconVisible ? mValueIfVisible : mValueIfGone;
}
}

View File

@@ -156,13 +156,24 @@ public class NotificationHeaderView extends FrameLayout {
* Sets the extra margin at the end of the top line of left-aligned text + icons.
* This value will have the margin required to accommodate the expand button added to it.
*
* @param extraMarginEnd extra margin
* @param extraMarginEnd extra margin in px
*/
@RemotableViewMethod
public void setTopLineExtraMarginEnd(int extraMarginEnd) {
mTopLineView.setHeaderTextMarginEnd(extraMarginEnd + mHeadingEndMargin);
}
/**
* Sets the extra margin at the end of the top line of left-aligned text + icons.
* This value will have the margin required to accommodate the expand button added to it.
*
* @param extraMarginEndDp extra margin in dp
*/
@RemotableViewMethod
public void setTopLineExtraMarginEndDp(float extraMarginEndDp) {
setTopLineExtraMarginEnd(
(int) (extraMarginEndDp * getResources().getDisplayMetrics().density));
}
/**
* Get the current margin end value for the header text.
* Add this to {@link #getTopLineBaseMarginEnd()} to get the total margin of the top line.

View File

@@ -20,6 +20,8 @@ import android.app.Notification;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.NotificationHeaderView;
import android.view.View;
import android.view.ViewGroup;
@@ -456,12 +458,14 @@ public class NotificationGroupingUtil {
if (target == null) {
return;
}
Integer value = (Integer) target.getTag(iconVisible
final Integer data = (Integer) target.getTag(iconVisible
? com.android.internal.R.id.tag_margin_end_when_icon_visible
: com.android.internal.R.id.tag_margin_end_when_icon_gone);
if (value == null) {
if (data == null) {
return;
}
final DisplayMetrics metrics = target.getResources().getDisplayMetrics();
final int value = TypedValue.complexToDimensionPixelOffset(data, metrics);
if (target instanceof NotificationHeaderView) {
((NotificationHeaderView) target).setTopLineExtraMarginEnd(value);
} else {

View File

@@ -116,7 +116,9 @@ import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.Mockito;
import org.mockito.internal.util.collections.Sets;
@@ -206,6 +208,16 @@ public class DevicePolicyManagerTest extends DpmTestBase {
private static final String PROFILE_OFF_SUSPENSION_TEXT = "suspension_text";
private static final String PROFILE_OFF_SUSPENSION_SOON_TEXT = "suspension_tomorrow_text";
@BeforeClass
public static void setUpClass() {
Notification.DevFlags.sForceDefaults = true;
}
@AfterClass
public static void tearDownClass() {
Notification.DevFlags.sForceDefaults = false;
}
@Before
public void setUp() throws Exception {

View File

@@ -17,6 +17,7 @@
package com.android.server.devicepolicy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.annotation.Nullable;
import android.app.AppOpsManager;
@@ -33,6 +34,7 @@ import android.os.Handler;
import android.os.UserHandle;
import android.test.mock.MockContext;
import android.util.ArrayMap;
import android.util.DisplayMetrics;
import android.util.ExceptionUtils;
import androidx.annotation.NonNull;
@@ -174,6 +176,11 @@ public class DpmMockContext extends MockContext {
binder = new MockBinder();
resources = mock(Resources.class);
spiedContext = mock(Context.class);
// Set up density for notification building
DisplayMetrics displayMetrics = mock(DisplayMetrics.class);
displayMetrics.density = 2.25f;
when(resources.getDisplayMetrics()).thenReturn(displayMetrics);
}
@Override

View File

@@ -20,6 +20,7 @@ import static com.android.server.connectivity.NetworkNotificationManager.Notific
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
@@ -39,6 +40,7 @@ import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.os.UserHandle;
import android.telephony.TelephonyManager;
import android.util.DisplayMetrics;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -46,7 +48,9 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.internal.R;
import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.AdditionalAnswers;
@@ -82,6 +86,7 @@ public class NetworkNotificationManagerTest {
@Mock Context mCtx;
@Mock Resources mResources;
@Mock DisplayMetrics mDisplayMetrics;
@Mock PackageManager mPm;
@Mock TelephonyManager mTelephonyManager;
@Mock NotificationManager mNotificationManager;
@@ -93,6 +98,17 @@ public class NetworkNotificationManagerTest {
NetworkNotificationManager mManager;
@BeforeClass
public static void setUpClass() {
Notification.DevFlags.sForceDefaults = true;
}
@AfterClass
public static void tearDownClass() {
Notification.DevFlags.sForceDefaults = false;
}
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
@@ -103,6 +119,7 @@ public class NetworkNotificationManagerTest {
mCellNai.networkInfo = mNetworkInfo;
mVpnNai.networkCapabilities = VPN_CAPABILITIES;
mVpnNai.networkInfo = mNetworkInfo;
mDisplayMetrics.density = 2.275f;
doReturn(true).when(mVpnNai).isVPN();
when(mCtx.getResources()).thenReturn(mResources);
when(mCtx.getPackageManager()).thenReturn(mPm);
@@ -114,6 +131,7 @@ public class NetworkNotificationManagerTest {
.thenReturn(mNotificationManager);
when(mNetworkInfo.getExtraInfo()).thenReturn("extra");
when(mResources.getColor(anyInt(), any())).thenReturn(0xFF607D8B);
when(mResources.getDisplayMetrics()).thenReturn(mDisplayMetrics);
mManager = new NetworkNotificationManager(mCtx, mTelephonyManager);
}
@@ -136,15 +154,15 @@ public class NetworkNotificationManagerTest {
public void testTitleOfPrivateDnsBroken() {
// Test the title of mobile data.
verifyTitleByNetwork(100, mCellNai, R.string.mobile_no_internet);
reset(mResources);
clearInvocations(mResources);
// Test the title of wifi.
verifyTitleByNetwork(101, mWifiNai, R.string.wifi_no_internet);
reset(mResources);
clearInvocations(mResources);
// Test the title of other networks.
verifyTitleByNetwork(102, mVpnNai, R.string.other_networks_no_internet);
reset(mResources);
clearInvocations(mResources);
}
@Test