From b8e05658946540774098848898a5047c56449a5d Mon Sep 17 00:00:00 2001 From: Steve Elliott Date: Wed, 25 Jan 2023 12:35:45 -0500 Subject: [PATCH] Enact minimum height requirement for notifications Notifications posted with a custom view that has too small of a total height will no longer be added to the notification shade. Fixes: 251586912 Test: atest NotificationContentInflaterTest Change-Id: I63df15221647c091aabf27ee8ff96ba62bbde4f4 --- .../layout/status_bar_notification_row.xml | 7 +- packages/SystemUI/res/values/dimens.xml | 9 +++ .../row/NotificationContentInflater.java | 72 +++++++++++++++++++ .../layout/invalid_notification_height.xml | 18 +++++ .../row/NotificationContentInflaterTest.java | 34 +++++++++ 5 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/tests/res/layout/invalid_notification_height.xml diff --git a/packages/SystemUI/res/layout/status_bar_notification_row.xml b/packages/SystemUI/res/layout/status_bar_notification_row.xml index 2c08f5db03233..356b36fdbcd65 100644 --- a/packages/SystemUI/res/layout/status_bar_notification_row.xml +++ b/packages/SystemUI/res/layout/status_bar_notification_row.xml @@ -39,8 +39,11 @@ + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="@dimen/notification_content_min_height" + android:gravity="center_vertical" + /> @*android:dimen/notification_min_height + + 10dp + + + 48dp + + + 320dp + 146dp diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index c534860d12c6d..39e4000c5d05c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -28,8 +28,11 @@ import android.content.Context; import android.content.ContextWrapper; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.content.res.Resources; import android.os.AsyncTask; +import android.os.Build; import android.os.CancellationSignal; +import android.os.Trace; import android.os.UserHandle; import android.service.notification.StatusBarNotification; import android.util.Log; @@ -38,6 +41,7 @@ import android.widget.RemoteViews; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.ImageMessageConsumer; +import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.media.controls.util.MediaFeatureFlag; @@ -468,6 +472,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder result.packageContext, parentLayout, remoteViewClickHandler); + validateView(v, entry, row.getResources()); v.setIsRootNamespace(true); applyCallback.setResultView(v); } else { @@ -475,6 +480,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder result.packageContext, existingView, remoteViewClickHandler); + validateView(existingView, entry, row.getResources()); existingWrapper.onReinflated(); } } catch (Exception e) { @@ -496,6 +502,13 @@ public class NotificationContentInflater implements NotificationRowContentBinder @Override public void onViewApplied(View v) { + String invalidReason = isValidView(v, entry, row.getResources()); + if (invalidReason != null) { + handleInflationError(runningInflations, new InflationException(invalidReason), + row.getEntry(), callback); + runningInflations.remove(inflationId); + return; + } if (isNewView) { v.setIsRootNamespace(true); applyCallback.setResultView(v); @@ -553,6 +566,65 @@ public class NotificationContentInflater implements NotificationRowContentBinder runningInflations.put(inflationId, cancellationSignal); } + /** + * Checks if the given View is a valid notification View. + * + * @return null == valid, non-null == invalid, String represents reason for rejection. + */ + @VisibleForTesting + @Nullable + static String isValidView(View view, + NotificationEntry entry, + Resources resources) { + if (!satisfiesMinHeightRequirement(view, entry, resources)) { + return "inflated notification does not meet minimum height requirement"; + } + return null; + } + + private static boolean satisfiesMinHeightRequirement(View view, + NotificationEntry entry, + Resources resources) { + if (!requiresHeightCheck(entry)) { + return true; + } + Trace.beginSection("NotificationContentInflater#satisfiesMinHeightRequirement"); + int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + int referenceWidth = resources.getDimensionPixelSize( + R.dimen.notification_validation_reference_width); + int widthSpec = View.MeasureSpec.makeMeasureSpec(referenceWidth, View.MeasureSpec.EXACTLY); + view.measure(widthSpec, heightSpec); + int minHeight = resources.getDimensionPixelSize( + R.dimen.notification_validation_minimum_allowed_height); + boolean result = view.getMeasuredHeight() >= minHeight; + Trace.endSection(); + return result; + } + + private static boolean requiresHeightCheck(NotificationEntry entry) { + // Undecorated custom views are disallowed from S onwards + if (entry.targetSdk >= Build.VERSION_CODES.S) { + return false; + } + // No need to check if the app isn't using any custom views + Notification notification = entry.getSbn().getNotification(); + if (notification.contentView == null + && notification.bigContentView == null + && notification.headsUpContentView == null) { + return false; + } + return true; + } + + private static void validateView(View view, + NotificationEntry entry, + Resources resources) throws InflationException { + String invalidReason = isValidView(view, entry, resources); + if (invalidReason != null) { + throw new InflationException(invalidReason); + } + } + private static void handleInflationError( HashMap runningInflations, Exception e, NotificationEntry notification, @Nullable InflationCallback callback) { diff --git a/packages/SystemUI/tests/res/layout/invalid_notification_height.xml b/packages/SystemUI/tests/res/layout/invalid_notification_height.xml new file mode 100644 index 0000000000000..aac43bf456767 --- /dev/null +++ b/packages/SystemUI/tests/res/layout/invalid_notification_height.xml @@ -0,0 +1,18 @@ + + + \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java index 5394d88ad1039..3face350526a1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java @@ -24,6 +24,7 @@ import static com.android.systemui.statusbar.notification.row.NotificationRowCon import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -42,6 +43,7 @@ import android.os.Looper; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; +import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; import android.widget.RemoteViews; @@ -332,6 +334,38 @@ public class NotificationContentInflaterTest extends SysuiTestCase { eq(FLAG_CONTENT_VIEW_HEADS_UP)); } + @Test + public void testNotificationViewHeightTooSmallFailsValidation() { + View view = mock(View.class); + when(view.getHeight()) + .thenReturn((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 10, + mContext.getResources().getDisplayMetrics())); + String result = NotificationContentInflater.isValidView(view, mRow.getEntry(), + mContext.getResources()); + assertNotNull(result); + } + + @Test + public void testNotificationViewPassesValidation() { + View view = mock(View.class); + when(view.getHeight()) + .thenReturn((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 17, + mContext.getResources().getDisplayMetrics())); + String result = NotificationContentInflater.isValidView(view, mRow.getEntry(), + mContext.getResources()); + assertNull(result); + } + + @Test + public void testInvalidNotificationDoesNotInvokeCallback() throws Exception { + mRow.getPrivateLayout().removeAllViews(); + mRow.getEntry().getSbn().getNotification().contentView = + new RemoteViews(mContext.getPackageName(), R.layout.invalid_notification_height); + inflateAndWait(true, mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); + assertEquals(0, mRow.getPrivateLayout().getChildCount()); + verify(mRow, times(0)).onNotificationUpdated(); + } + private static void inflateAndWait(NotificationContentInflater inflater, @InflationFlag int contentToInflate, ExpandableNotificationRow row)