Merge "Enact minimum height requirement for notifications" into tm-qpr-dev am: 0f6acd6fb7
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/21097606 Change-Id: Iecd45ead5c116da1da5b52c3ce42b0b5fec573c6 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -39,8 +39,11 @@
|
|||||||
|
|
||||||
<com.android.systemui.statusbar.notification.row.NotificationContentView
|
<com.android.systemui.statusbar.notification.row.NotificationContentView
|
||||||
android:id="@+id/expanded"
|
android:id="@+id/expanded"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content"
|
||||||
|
android:minHeight="@dimen/notification_content_min_height"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
/>
|
||||||
|
|
||||||
<com.android.systemui.statusbar.notification.row.NotificationContentView
|
<com.android.systemui.statusbar.notification.row.NotificationContentView
|
||||||
android:id="@+id/expandedPublic"
|
android:id="@+id/expandedPublic"
|
||||||
|
|||||||
@@ -184,6 +184,15 @@
|
|||||||
<!-- Height of a small notification in the status bar-->
|
<!-- Height of a small notification in the status bar-->
|
||||||
<dimen name="notification_min_height">@*android:dimen/notification_min_height</dimen>
|
<dimen name="notification_min_height">@*android:dimen/notification_min_height</dimen>
|
||||||
|
|
||||||
|
<!-- Minimum allowed height of notifications -->
|
||||||
|
<dimen name="notification_validation_minimum_allowed_height">10dp</dimen>
|
||||||
|
|
||||||
|
<!-- Minimum height for displaying notification content. -->
|
||||||
|
<dimen name="notification_content_min_height">48dp</dimen>
|
||||||
|
|
||||||
|
<!-- Reference width used when validating notification layouts -->
|
||||||
|
<dimen name="notification_validation_reference_width">320dp</dimen>
|
||||||
|
|
||||||
<!-- Increased height of a small notification in the status bar -->
|
<!-- Increased height of a small notification in the status bar -->
|
||||||
<dimen name="notification_min_height_increased">146dp</dimen>
|
<dimen name="notification_min_height_increased">146dp</dimen>
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,11 @@ import android.content.Context;
|
|||||||
import android.content.ContextWrapper;
|
import android.content.ContextWrapper;
|
||||||
import android.content.pm.ApplicationInfo;
|
import android.content.pm.ApplicationInfo;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
import android.content.res.Resources;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.CancellationSignal;
|
import android.os.CancellationSignal;
|
||||||
|
import android.os.Trace;
|
||||||
import android.os.UserHandle;
|
import android.os.UserHandle;
|
||||||
import android.service.notification.StatusBarNotification;
|
import android.service.notification.StatusBarNotification;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@@ -38,6 +41,7 @@ import android.widget.RemoteViews;
|
|||||||
|
|
||||||
import com.android.internal.annotations.VisibleForTesting;
|
import com.android.internal.annotations.VisibleForTesting;
|
||||||
import com.android.internal.widget.ImageMessageConsumer;
|
import com.android.internal.widget.ImageMessageConsumer;
|
||||||
|
import com.android.systemui.R;
|
||||||
import com.android.systemui.dagger.SysUISingleton;
|
import com.android.systemui.dagger.SysUISingleton;
|
||||||
import com.android.systemui.dagger.qualifiers.Background;
|
import com.android.systemui.dagger.qualifiers.Background;
|
||||||
import com.android.systemui.media.controls.util.MediaFeatureFlag;
|
import com.android.systemui.media.controls.util.MediaFeatureFlag;
|
||||||
@@ -468,6 +472,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder
|
|||||||
result.packageContext,
|
result.packageContext,
|
||||||
parentLayout,
|
parentLayout,
|
||||||
remoteViewClickHandler);
|
remoteViewClickHandler);
|
||||||
|
validateView(v, entry, row.getResources());
|
||||||
v.setIsRootNamespace(true);
|
v.setIsRootNamespace(true);
|
||||||
applyCallback.setResultView(v);
|
applyCallback.setResultView(v);
|
||||||
} else {
|
} else {
|
||||||
@@ -475,6 +480,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder
|
|||||||
result.packageContext,
|
result.packageContext,
|
||||||
existingView,
|
existingView,
|
||||||
remoteViewClickHandler);
|
remoteViewClickHandler);
|
||||||
|
validateView(existingView, entry, row.getResources());
|
||||||
existingWrapper.onReinflated();
|
existingWrapper.onReinflated();
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -496,6 +502,13 @@ public class NotificationContentInflater implements NotificationRowContentBinder
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewApplied(View v) {
|
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) {
|
if (isNewView) {
|
||||||
v.setIsRootNamespace(true);
|
v.setIsRootNamespace(true);
|
||||||
applyCallback.setResultView(v);
|
applyCallback.setResultView(v);
|
||||||
@@ -553,6 +566,65 @@ public class NotificationContentInflater implements NotificationRowContentBinder
|
|||||||
runningInflations.put(inflationId, cancellationSignal);
|
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(
|
private static void handleInflationError(
|
||||||
HashMap<Integer, CancellationSignal> runningInflations, Exception e,
|
HashMap<Integer, CancellationSignal> runningInflations, Exception e,
|
||||||
NotificationEntry notification, @Nullable InflationCallback callback) {
|
NotificationEntry notification, @Nullable InflationCallback callback) {
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright (C) 2023 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.
|
||||||
|
-->
|
||||||
|
<View xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="5dp"/>
|
||||||
@@ -24,6 +24,7 @@ import static com.android.systemui.statusbar.notification.row.NotificationRowCon
|
|||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertNotEquals;
|
import static org.junit.Assert.assertNotEquals;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
@@ -42,6 +43,7 @@ import android.os.Looper;
|
|||||||
import android.testing.AndroidTestingRunner;
|
import android.testing.AndroidTestingRunner;
|
||||||
import android.testing.TestableLooper;
|
import android.testing.TestableLooper;
|
||||||
import android.testing.TestableLooper.RunWithLooper;
|
import android.testing.TestableLooper.RunWithLooper;
|
||||||
|
import android.util.TypedValue;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.RemoteViews;
|
import android.widget.RemoteViews;
|
||||||
@@ -332,6 +334,38 @@ public class NotificationContentInflaterTest extends SysuiTestCase {
|
|||||||
eq(FLAG_CONTENT_VIEW_HEADS_UP));
|
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,
|
private static void inflateAndWait(NotificationContentInflater inflater,
|
||||||
@InflationFlag int contentToInflate,
|
@InflationFlag int contentToInflate,
|
||||||
ExpandableNotificationRow row)
|
ExpandableNotificationRow row)
|
||||||
|
|||||||
Reference in New Issue
Block a user