Merge "Add a people tile state for dnd" into sc-dev

This commit is contained in:
Stevie Kideckel
2021-06-24 17:51:26 +00:00
committed by Android (Google) Code Review
7 changed files with 343 additions and 18 deletions

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 2021 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:id="@+id/item"
android:background="@drawable/people_tile_suppressed_background"
android:clipToOutline="true"
android:padding="8dp"
android:orientation="horizontal">
<ImageView
android:id="@+id/person_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:gravity="start"
android:id="@+id/text_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:ellipsize="end"
android:maxLines="2"
android:singleLine="false"
android:text="@string/empty_status"
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title"
android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/content_text_size_for_medium" />
</LinearLayout>

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 2021 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:id="@+id/item"
android:background="@drawable/people_tile_suppressed_background"
android:clipToOutline="true"
android:padding="8dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"/>
<ImageView
android:id="@+id/person_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:gravity="center"
android:id="@+id/text_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:singleLine="false"
android:text="@string/empty_status"
android:layout_marginTop="@dimen/padding_between_suppressed_layout_items"
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/content_text_size_for_large" />
<ImageView
android:id="@+id/predefined_icon"
android:tint="?android:attr/textColorSecondary"
android:layout_marginTop="@dimen/padding_between_suppressed_layout_items"
android:layout_width="@dimen/regular_predefined_icon"
android:layout_height="@dimen/regular_predefined_icon"
tools:ignore="UseAppTint" />
<TextView
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>

View File

@@ -1451,7 +1451,7 @@
<dimen name="people_space_messages_count_radius">12dp</dimen>
<dimen name="people_space_widget_background_padding">6dp</dimen>
<dimen name="required_width_for_medium">136dp</dimen>
<dimen name="required_width_for_large">138dp</dimen>
<dimen name="required_width_for_large">120dp</dimen>
<dimen name="required_height_for_large">168dp</dimen>
<dimen name="default_width">146dp</dimen>
<dimen name="default_height">92dp</dimen>
@@ -1470,10 +1470,12 @@
<dimen name="below_name_text_padding">16dp</dimen>
<dimen name="above_notification_text_padding">22dp</dimen>
<dimen name="regular_predefined_icon">18dp</dimen>
<dimen name="large_predefined_icon">24dp</dimen>
<dimen name="larger_predefined_icon">24dp</dimen>
<dimen name="largest_predefined_icon">32dp</dimen>
<dimen name="availability_dot_status_padding">8dp</dimen>
<dimen name="availability_dot_notification_padding">12dp</dimen>
<dimen name="medium_content_padding_above_name">4dp</dimen>
<dimen name="padding_between_suppressed_layout_items">8dp</dimen>
<!-- Accessibility floating menu -->
<dimen name="accessibility_floating_menu_elevation">3dp</dimen>

View File

@@ -2952,6 +2952,8 @@
<string name="people_tile_description">See recent messages, missed calls, and status updates</string>
<!-- Title text displayed for the Conversation widget [CHAR LIMIT=50] -->
<string name="people_tile_title">Conversation</string>
<!-- Text when the Conversation widget when Do Not Disturb is suppressing the notification. [CHAR LIMIT=50] -->
<string name="paused_by_dnd">Paused by Do Not Disturb</string>
<!-- Content description text on the Conversation widget when a person has sent a new text message [CHAR LIMIT=150] -->
<string name="new_notification_text_content_description"><xliff:g id="name" example="Anna">%1$s</xliff:g> sent a message</string>
<!-- Content description text on the Conversation widget when a person has sent a new image message [CHAR LIMIT=150] -->

View File

@@ -211,7 +211,8 @@ class PeopleStoryIconFactory implements AutoCloseable {
@Override
public void setColorFilter(ColorFilter colorFilter) {
// unimplemented
if (mAvatar != null) mAvatar.setColorFilter(colorFilter);
if (mBadgeIcon != null) mBadgeIcon.setColorFilter(colorFilter);
}
@Override

View File

@@ -29,6 +29,8 @@ import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT;
import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH;
import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT;
import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH;
import static android.util.TypedValue.COMPLEX_UNIT_DIP;
import static android.util.TypedValue.COMPLEX_UNIT_PX;
import static com.android.systemui.people.PeopleSpaceUtils.STARRED_CONTACT;
import static com.android.systemui.people.PeopleSpaceUtils.VALID_CONTACT;
@@ -43,11 +45,16 @@ import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.graphics.text.LineBreaker;
import android.net.Uri;
import android.os.Bundle;
import android.os.UserHandle;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.IconDrawableFactory;
import android.util.Log;
@@ -58,8 +65,11 @@ import android.view.View;
import android.widget.RemoteViews;
import android.widget.TextView;
import androidx.annotation.DimenRes;
import androidx.annotation.Px;
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
import androidx.core.math.MathUtils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.launcher3.icons.FastBitmapDrawable;
@@ -197,12 +207,16 @@ public class PeopleTileViewHelper {
*/
private RemoteViews getViewForTile() {
if (DEBUG) Log.d(TAG, "Creating view for tile key: " + mKey.toString());
if (mTile == null || mTile.isPackageSuspended() || mTile.isUserQuieted()
|| isDndBlockingTileData(mTile)) {
if (mTile == null || mTile.isPackageSuspended() || mTile.isUserQuieted()) {
if (DEBUG) Log.d(TAG, "Create suppressed view: " + mTile);
return createSuppressedView();
}
if (isDndBlockingTileData(mTile)) {
if (DEBUG) Log.d(TAG, "Create dnd view");
return createDndRemoteViews().mRemoteViews;
}
if (Objects.equals(mTile.getNotificationCategory(), CATEGORY_MISSED_CALL)) {
if (DEBUG) Log.d(TAG, "Create missed call view");
return createMissedCallRemoteViews();
@@ -236,7 +250,9 @@ public class PeopleTileViewHelper {
return createLastInteractionRemoteViews();
}
private boolean isDndBlockingTileData(PeopleSpaceTile tile) {
private static boolean isDndBlockingTileData(@Nullable PeopleSpaceTile tile) {
if (tile == null) return false;
int notificationPolicyState = tile.getNotificationPolicyState();
if ((notificationPolicyState & PeopleSpaceTile.SHOW_CONVERSATIONS) != 0) {
// Not in DND, or all conversations
@@ -413,6 +429,11 @@ public class PeopleTileViewHelper {
int avatarWidthSpace = mWidth - (14 + 14);
avatarSize = Math.min(avatarHeightSpace, avatarWidthSpace);
}
if (isDndBlockingTileData(mTile)) {
avatarSize = createDndRemoteViews().mAvatarSize;
}
return Math.min(avatarSize,
getSizeInDp(R.dimen.max_people_avatar_size));
}
@@ -473,6 +494,87 @@ public class PeopleTileViewHelper {
return views;
}
private RemoteViewsAndSizes createDndRemoteViews() {
boolean isHorizontal = mLayoutSize == LAYOUT_MEDIUM;
int layoutId = isHorizontal
? R.layout.people_tile_with_suppression_detail_content_horizontal
: R.layout.people_tile_with_suppression_detail_content_vertical;
RemoteViews views = new RemoteViews(mContext.getPackageName(), layoutId);
int outerPadding = mLayoutSize == LAYOUT_LARGE ? 16 : 8;
int outerPaddingPx = dpToPx(outerPadding);
views.setViewPadding(
R.id.item,
outerPaddingPx,
outerPaddingPx,
outerPaddingPx,
outerPaddingPx);
int mediumAvatarSize = getSizeInDp(R.dimen.avatar_size_for_medium);
int maxAvatarSize = getSizeInDp(R.dimen.max_people_avatar_size);
String text = mContext.getString(R.string.paused_by_dnd);
views.setTextViewText(R.id.text_content, text);
int textSizeResId =
mLayoutSize == LAYOUT_LARGE
? R.dimen.content_text_size_for_large
: R.dimen.content_text_size_for_medium;
float textSizePx = mContext.getResources().getDimension(textSizeResId);
views.setTextViewTextSize(R.id.text_content, COMPLEX_UNIT_PX, textSizePx);
int lineHeight = getLineHeightFromResource(textSizeResId);
int avatarSize;
if (isHorizontal) {
int maxTextHeight = mHeight - outerPadding;
views.setInt(R.id.text_content, "setMaxLines", maxTextHeight / lineHeight);
avatarSize = mediumAvatarSize;
} else {
int iconSize =
getSizeInDp(
mLayoutSize == LAYOUT_SMALL
? R.dimen.regular_predefined_icon
: R.dimen.largest_predefined_icon);
int heightWithoutIcon = mHeight - 2 * outerPadding - iconSize;
int paddingBetweenElements =
getSizeInDp(R.dimen.padding_between_suppressed_layout_items);
int maxTextWidth = mWidth - outerPadding * 2;
int maxTextHeight = heightWithoutIcon - mediumAvatarSize - paddingBetweenElements * 2;
int availableAvatarHeight;
int textHeight = estimateTextHeight(text, textSizeResId, maxTextWidth);
if (textHeight <= maxTextHeight) {
// If the text will fit, then display it and deduct its height from the space we
// have for the avatar.
availableAvatarHeight = heightWithoutIcon - textHeight - paddingBetweenElements * 2;
views.setViewVisibility(R.id.text_content, View.VISIBLE);
views.setInt(R.id.text_content, "setMaxLines", maxTextHeight / lineHeight);
views.setContentDescription(R.id.predefined_icon, null);
} else {
// If the height doesn't fit, then hide it. The dnd icon will still show.
availableAvatarHeight = heightWithoutIcon - paddingBetweenElements;
views.setViewVisibility(R.id.text_content, View.GONE);
// If we don't show the dnd text, set it as the content description on the icon
// for a11y.
views.setContentDescription(R.id.predefined_icon, text);
}
int availableAvatarWidth = mWidth - outerPadding * 2;
avatarSize =
MathUtils.clamp(
/* value= */ Math.min(availableAvatarWidth, availableAvatarHeight),
/* min= */ dpToPx(10),
/* max= */ maxAvatarSize);
views.setViewLayoutWidth(R.id.predefined_icon, iconSize, COMPLEX_UNIT_DIP);
views.setViewLayoutHeight(R.id.predefined_icon, iconSize, COMPLEX_UNIT_DIP);
views.setImageViewResource(R.id.predefined_icon, R.drawable.ic_qs_dnd_on);
}
return new RemoteViewsAndSizes(views, avatarSize);
}
private RemoteViews createMissedCallRemoteViews() {
RemoteViews views = setViewForContentLayout(new RemoteViews(mContext.getPackageName(),
getLayoutForContent()));
@@ -486,8 +588,8 @@ public class PeopleTileViewHelper {
views.setImageViewResource(R.id.predefined_icon, R.drawable.ic_phone_missed);
if (mLayoutSize == LAYOUT_LARGE) {
views.setInt(R.id.content, "setGravity", Gravity.BOTTOM);
views.setViewLayoutHeightDimen(R.id.predefined_icon, R.dimen.large_predefined_icon);
views.setViewLayoutWidthDimen(R.id.predefined_icon, R.dimen.large_predefined_icon);
views.setViewLayoutHeightDimen(R.id.predefined_icon, R.dimen.larger_predefined_icon);
views.setViewLayoutWidthDimen(R.id.predefined_icon, R.dimen.larger_predefined_icon);
}
setAvailabilityDotPadding(views, R.dimen.availability_dot_notification_padding);
return views;
@@ -932,6 +1034,14 @@ public class PeopleTileViewHelper {
Drawable personDrawable = storyIcon.getPeopleTileDrawable(roundedDrawable,
tile.getPackageName(), getUserId(tile), tile.isImportantConversation(),
hasNewStory);
if (isDndBlockingTileData(tile)) {
// If DND is blocking the conversation, then display the icon in grayscale.
ColorMatrix colorMatrix = new ColorMatrix();
colorMatrix.setSaturation(0);
personDrawable.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
}
return convertDrawableToBitmap(personDrawable);
}
@@ -960,4 +1070,69 @@ public class PeopleTileViewHelper {
return context.getString(R.string.over_two_weeks_timestamp);
}
}
/**
* Estimates the height (in dp) which the text will have given the text size and the available
* width. Returns Integer.MAX_VALUE if the estimation couldn't be obtained, as this is intended
* to be used an estimate of the maximum.
*/
private int estimateTextHeight(
CharSequence text,
@DimenRes int textSizeResId,
int availableWidthDp) {
StaticLayout staticLayout = buildStaticLayout(text, textSizeResId, availableWidthDp);
if (staticLayout == null) {
// Return max value (rather than e.g. -1) so the value can be used with <= bound checks.
return Integer.MAX_VALUE;
}
return pxToDp(staticLayout.getHeight());
}
/**
* Builds a StaticLayout for the text given the text size and available width. This can be used
* to obtain information about how TextView will lay out the text. Returns null if any error
* occurred creating a TextView.
*/
@Nullable
private StaticLayout buildStaticLayout(
CharSequence text,
@DimenRes int textSizeResId,
int availableWidthDp) {
try {
TextView textView = new TextView(mContext);
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX,
mContext.getResources().getDimension(textSizeResId));
textView.setTextAppearance(android.R.style.TextAppearance_DeviceDefault);
TextPaint paint = textView.getPaint();
return StaticLayout.Builder.obtain(
text, 0, text.length(), paint, dpToPx(availableWidthDp))
// Simple break strategy avoids hyphenation unless there's a single word longer
// than the line width. We use this break strategy so that we consider text to
// "fit" only if it fits in a nice way (i.e. without hyphenation in the middle
// of words).
.setBreakStrategy(LineBreaker.BREAK_STRATEGY_SIMPLE)
.build();
} catch (Exception e) {
Log.e(TAG, "Could not create static layout: " + e);
return null;
}
}
private int dpToPx(float dp) {
return (int) (dp * mDensity);
}
private int pxToDp(@Px float px) {
return (int) (px / mDensity);
}
private static final class RemoteViewsAndSizes {
final RemoteViews mRemoteViews;
final int mAvatarSize;
RemoteViewsAndSizes(RemoteViews remoteViews, int avatarSize) {
mRemoteViews = remoteViews;
mAvatarSize = avatarSize;
}
}
}

View File

@@ -35,7 +35,6 @@ import static com.android.systemui.people.PeopleSpaceUtils.VALID_CONTACT;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -541,7 +540,9 @@ public class PeopleTileViewHelperTest extends SysuiTestCase {
tileWithDndBlocking, mOptions).getViews();
View result = views.apply(mContext, null);
assertEquals(result.getSourceLayoutResId(), R.layout.people_tile_suppressed_layout);
assertResourcesEqual(
result.getSourceLayoutResId(),
R.layout.people_tile_with_suppression_detail_content_horizontal);
tileWithDndBlocking = PERSON_TILE.toBuilder()
.setNotificationPolicyState(BLOCK_CONVERSATIONS)
@@ -551,7 +552,9 @@ public class PeopleTileViewHelperTest extends SysuiTestCase {
tileWithDndBlocking, mOptions).getViews();
result = views.apply(mContext, null);
assertNotEquals(result.getSourceLayoutResId(), R.layout.people_tile_suppressed_layout);
assertResourcesNotEqual(
result.getSourceLayoutResId(),
R.layout.people_tile_with_suppression_detail_content_horizontal);
tileWithDndBlocking = PERSON_TILE.toBuilder()
.setNotificationPolicyState(SHOW_IMPORTANT_CONVERSATIONS)
@@ -560,7 +563,9 @@ public class PeopleTileViewHelperTest extends SysuiTestCase {
tileWithDndBlocking, mOptions).getViews();
result = views.apply(mContext, null);
assertEquals(result.getSourceLayoutResId(), R.layout.people_tile_suppressed_layout);
assertResourcesEqual(
result.getSourceLayoutResId(),
R.layout.people_tile_with_suppression_detail_content_horizontal);
tileWithDndBlocking = PERSON_TILE.toBuilder()
.setNotificationPolicyState(SHOW_IMPORTANT_CONVERSATIONS)
@@ -570,7 +575,9 @@ public class PeopleTileViewHelperTest extends SysuiTestCase {
tileWithDndBlocking, mOptions).getViews();
result = views.apply(mContext, null);
assertNotEquals(result.getSourceLayoutResId(), R.layout.people_tile_suppressed_layout);
assertResourcesNotEqual(
result.getSourceLayoutResId(),
R.layout.people_tile_with_suppression_detail_content_horizontal);
tileWithDndBlocking = PERSON_TILE.toBuilder()
.setNotificationPolicyState(SHOW_STARRED_CONTACTS)
@@ -580,7 +587,9 @@ public class PeopleTileViewHelperTest extends SysuiTestCase {
tileWithDndBlocking, mOptions).getViews();
result = views.apply(mContext, null);
assertEquals(result.getSourceLayoutResId(), R.layout.people_tile_suppressed_layout);
assertResourcesEqual(
result.getSourceLayoutResId(),
R.layout.people_tile_with_suppression_detail_content_horizontal);
tileWithDndBlocking = PERSON_TILE.toBuilder()
.setNotificationPolicyState(SHOW_STARRED_CONTACTS)
@@ -590,7 +599,9 @@ public class PeopleTileViewHelperTest extends SysuiTestCase {
tileWithDndBlocking, mOptions).getViews();
result = views.apply(mContext, null);
assertNotEquals(result.getSourceLayoutResId(), R.layout.people_tile_suppressed_layout);
assertResourcesNotEqual(
result.getSourceLayoutResId(),
R.layout.people_tile_with_suppression_detail_content_horizontal);
tileWithDndBlocking = PERSON_TILE.toBuilder()
.setNotificationPolicyState(SHOW_CONTACTS)
@@ -600,7 +611,9 @@ public class PeopleTileViewHelperTest extends SysuiTestCase {
tileWithDndBlocking, mOptions).getViews();
result = views.apply(mContext, null);
assertNotEquals(result.getSourceLayoutResId(), R.layout.people_tile_suppressed_layout);
assertResourcesNotEqual(
result.getSourceLayoutResId(),
R.layout.people_tile_with_suppression_detail_content_horizontal);
tileWithDndBlocking = PERSON_TILE.toBuilder()
.setNotificationPolicyState(SHOW_CONTACTS)
@@ -610,7 +623,9 @@ public class PeopleTileViewHelperTest extends SysuiTestCase {
tileWithDndBlocking, mOptions).getViews();
result = views.apply(mContext, null);
assertNotEquals(result.getSourceLayoutResId(), R.layout.people_tile_suppressed_layout);
assertResourcesNotEqual(
result.getSourceLayoutResId(),
R.layout.people_tile_with_suppression_detail_content_horizontal);
tileWithDndBlocking = PERSON_TILE.toBuilder()
.setNotificationPolicyState(SHOW_CONTACTS)
@@ -619,7 +634,11 @@ public class PeopleTileViewHelperTest extends SysuiTestCase {
tileWithDndBlocking, mOptions).getViews();
result = views.apply(mContext, null);
assertEquals(result.getSourceLayoutResId(), R.layout.people_tile_suppressed_layout);
assertResourcesEqual(
result.getSourceLayoutResId(),
R.layout.people_tile_with_suppression_detail_content_horizontal);
assertThat(result.<TextView>findViewById(R.id.text_content).getText().toString())
.isEqualTo(mContext.getString(R.string.paused_by_dnd));
}
@Test
@@ -1068,4 +1087,21 @@ public class PeopleTileViewHelperTest extends SysuiTestCase {
return new PeopleTileViewHelper(mContext, tile, 0, options,
new PeopleTileKey(tile.getId(), 0, tile.getPackageName()));
}
private void assertResourcesEqual(int expected, int actual) {
assertThat(getResourceName(actual)).isEqualTo(getResourceName(expected));
}
private void assertResourcesNotEqual(int expected, int actual) {
assertThat(getResourceName(actual)).isNotEqualTo(getResourceName(expected));
}
private String getResourceName(int resId) {
Resources resources = mContext.getResources();
try {
return resources.getResourceEntryName(resId);
} catch (Resources.NotFoundException e) {
return String.valueOf(resId);
}
}
}