Show icons for allowed sounds

(Some icons are temporary, until we get the final assets)

Also some fixes/improvements to CircularIconsPreference:
* Show the correct placeholder and +N icons.
* Fix the displayIcons-before-measure case (global layout listener was incorrect).
* Properly cancel pending image load futures (field didn't point to the actual future).
* Don't reload icons if it's the same set (depends on equals() for the items, so unfortunately doesn't work for AppEntry yet).

Test: atest com.android.settings.notification.modes
Bug: 346551087
Flag: android.app.modes_ui
Change-Id: I9d029a5fdd785ada4e2ba4d8a90eba72b5fb9085
This commit is contained in:
Matías Hernández
2024-07-18 11:32:21 +02:00
parent 0ebc865c5e
commit 352100397f
16 changed files with 389 additions and 105 deletions

View File

@@ -0,0 +1,28 @@
<!--
~ Copyright (C) 2024 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.
-->
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:shape="oval">
<size
android:width="@dimen/zen_mode_circular_icon_diameter"
android:height="@dimen/zen_mode_circular_icon_diameter" />
<solid android:color="?androidprv:attr/materialColorSecondaryContainer" />
<!-- TODO: b/346551087 - Include border (or not) according to final design
<stroke android:width="1dp" android:color="?androidprv:attr/materialColorOnSecondaryContainer" />
-->
</shape>

View File

@@ -17,8 +17,8 @@
<ImageView <ImageView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="@dimen/zen_mode_circular_icon_size" android:layout_width="@dimen/zen_mode_circular_icon_diameter"
android:layout_height="@dimen/zen_mode_circular_icon_size" android:layout_height="@dimen/zen_mode_circular_icon_diameter"
android:layout_marginTop="@dimen/zen_mode_circular_icon_margin_vertical" android:layout_marginTop="@dimen/zen_mode_circular_icon_margin_vertical"
android:layout_marginBottom="@dimen/zen_mode_circular_icon_margin_vertical" android:layout_marginBottom="@dimen/zen_mode_circular_icon_margin_vertical"
android:layout_marginEnd="@dimen/zen_mode_circular_icon_margin_between" /> android:layout_marginEnd="@dimen/zen_mode_circular_icon_margin_between" />

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2024 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.
-->
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:layout_width="@dimen/zen_mode_circular_icon_diameter"
android:layout_height="@dimen/zen_mode_circular_icon_diameter"
android:layout_marginTop="@dimen/zen_mode_circular_icon_margin_vertical"
android:layout_marginBottom="@dimen/zen_mode_circular_icon_margin_vertical"
android:gravity="center"
android:padding="4dp"
android:drawablePadding="0dp"
android:background="@drawable/preference_circular_icons_plus_item_background"
android:textColor="?androidprv:attr/materialColorOnSecondaryContainer"
android:maxLines="1"
android:autoSizeTextType="uniform"
android:autoSizeMinTextSize="6sp" />

View File

@@ -509,7 +509,8 @@
<dimen name="zen_mode_icon_list_item_circle_diameter">56dp</dimen> <dimen name="zen_mode_icon_list_item_circle_diameter">56dp</dimen>
<dimen name="zen_mode_icon_list_item_icon_size">32dp</dimen> <dimen name="zen_mode_icon_list_item_icon_size">32dp</dimen>
<!-- For the items in the CircularIconsPreference (contacts, apps, sound channels). --> <!-- For the items in the CircularIconsPreference (contacts, apps, sound channels). -->
<dimen name="zen_mode_circular_icon_size">32dp</dimen> <dimen name="zen_mode_circular_icon_diameter">32dp</dimen>
<dimen name="zen_mode_circular_icon_inner_icon_size">20dp</dimen>
<dimen name="zen_mode_circular_icon_margin_between">4dp</dimen> <dimen name="zen_mode_circular_icon_margin_between">4dp</dimen>
<dimen name="zen_mode_circular_icon_margin_vertical">8dp</dimen> <dimen name="zen_mode_circular_icon_margin_vertical">8dp</dimen>
</resources> </resources>

View File

@@ -9361,6 +9361,8 @@
<string name="zen_mode_apps_work_app"><xliff:g id="app_label" example="Chrome">%s</xliff:g> (Work)</string> <string name="zen_mode_apps_work_app"><xliff:g id="app_label" example="Chrome">%s</xliff:g> (Work)</string>
<!-- Text displayed (for a brief time) while the list of bypassing apps is being fetched. Will be replaced by a zen_mode_apps_subtext. [CHAR_LIMIT=60] --> <!-- Text displayed (for a brief time) while the list of bypassing apps is being fetched. Will be replaced by a zen_mode_apps_subtext. [CHAR_LIMIT=60] -->
<string name="zen_mode_apps_calculating">Calculating\u2026</string> <string name="zen_mode_apps_calculating">Calculating\u2026</string>
<!-- Priority Modes: Format for a string displayed when there are more items (e.g. apps, contacts) that can be shown. For example, we show (A)(B)(C)(+5), where this string represents the "+5" value. Needs to be as compact as possible, since it will be drawn in a really small area. [CHAR_LIMIT=4] -->
<string name="zen_mode_plus_n_items">+<xliff:g id="number" example="42">%d</xliff:g></string>
<!-- [CHAR LIMIT=100] Zen mode settings: Allow apps to bypass DND --> <!-- [CHAR LIMIT=100] Zen mode settings: Allow apps to bypass DND -->
<string name="zen_mode_bypassing_apps">Allow apps to override</string> <string name="zen_mode_bypassing_apps">Allow apps to override</string>

View File

@@ -22,6 +22,7 @@ import android.graphics.drawable.Drawable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.ListeningExecutorService;
@@ -61,6 +62,15 @@ class CircularIconSet<T> {
mCachedIcons = new ConcurrentHashMap<>(); mCachedIcons = new ConcurrentHashMap<>();
} }
@Override
public String toString() {
return MoreObjects.toStringHelper(this).add("items", mItems).toString();
}
boolean hasSameItemsAs(CircularIconSet<?> other) {
return other != null && this.mItems.equals(other.mItems);
}
int size() { int size() {
return mItems.size(); return mItems.size();
} }

View File

@@ -21,15 +21,17 @@ import static com.google.common.base.Preconditions.checkState;
import android.content.Context; import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver; import android.view.ViewTreeObserver;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
@@ -37,11 +39,12 @@ import androidx.preference.PreferenceViewHolder;
import com.android.settings.R; import com.android.settings.R;
import com.android.settingslib.RestrictedPreference; import com.android.settingslib.RestrictedPreference;
import com.android.settingslib.Utils;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@@ -50,8 +53,9 @@ public class CircularIconsPreference extends RestrictedPreference {
private Executor mUiExecutor; private Executor mUiExecutor;
@Nullable private LinearLayout mIconContainer; @Nullable private LinearLayout mIconContainer;
@Nullable private CircularIconSet<?> mPendingIconSet; @Nullable private CircularIconSet<?> mIconSet;
@Nullable private ListenableFuture<?> mPendingLoadIconsFuture; @Nullable private CircularIconSet<?> mPendingDisplayIconSet;
@Nullable private ListenableFuture<List<Drawable>> mPendingLoadIconsFuture;
public CircularIconsPreference(Context context) { public CircularIconsPreference(Context context) {
super(context); super(context);
@@ -94,17 +98,25 @@ public class CircularIconsPreference extends RestrictedPreference {
} }
private void displayIconsIfPending() { private void displayIconsIfPending() {
CircularIconSet<?> pendingIconSet = mPendingIconSet; CircularIconSet<?> pendingIconSet = mPendingDisplayIconSet;
if (pendingIconSet != null) { if (pendingIconSet != null) {
mPendingIconSet = null; mPendingDisplayIconSet = null;
displayIcons(pendingIconSet); displayIconsInternal(pendingIconSet);
} }
} }
void displayIcons(CircularIconSet<?> iconSet) { void displayIcons(CircularIconSet<?> iconSet) {
if (mIconSet != null && mIconSet.hasSameItemsAs(iconSet)) {
return;
}
mIconSet = iconSet;
displayIconsInternal(iconSet);
}
void displayIconsInternal(CircularIconSet<?> iconSet) {
if (mIconContainer == null) { if (mIconContainer == null) {
// Too soon, wait for bind. // Too soon, wait for bind.
mPendingIconSet = iconSet; mPendingDisplayIconSet = iconSet;
return; return;
} }
mIconContainer.setVisibility(iconSet.size() != 0 ? View.VISIBLE : View.GONE); mIconContainer.setVisibility(iconSet.size() != 0 ? View.VISIBLE : View.GONE);
@@ -113,30 +125,31 @@ public class CircularIconsPreference extends RestrictedPreference {
} }
if (mIconContainer.getMeasuredWidth() == 0) { if (mIconContainer.getMeasuredWidth() == 0) {
// Too soon, wait for first measure to know width. // Too soon, wait for first measure to know width.
mPendingIconSet = iconSet; mPendingDisplayIconSet = iconSet;
ViewTreeObserver vto = mIconContainer.getViewTreeObserver(); mIconContainer.getViewTreeObserver().addOnGlobalLayoutListener(
vto.addOnGlobalLayoutListener(() ->
new ViewTreeObserver.OnGlobalLayoutListener() { new ViewTreeObserver.OnGlobalLayoutListener() {
@Override @Override
public void onGlobalLayout() { public void onGlobalLayout() {
vto.removeOnGlobalLayoutListener(this); checkNotNull(mIconContainer).getViewTreeObserver()
.removeOnGlobalLayoutListener(this);
displayIconsIfPending(); displayIconsIfPending();
} }
}); }
);
return; return;
} }
mIconContainer.setVisibility(View.VISIBLE); mIconContainer.setVisibility(View.VISIBLE);
Resources res = getContext().getResources(); Resources res = getContext().getResources();
int availableSpace = mIconContainer.getMeasuredWidth(); int availableSpace = mIconContainer.getMeasuredWidth();
int iconHorizontalSpace = res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_size) int iconHorizontalSpace = res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_diameter)
+ res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_margin_between); + res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_margin_between);
int numIconsThatFit = availableSpace / iconHorizontalSpace; int numIconsThatFit = availableSpace / iconHorizontalSpace;
List<ListenableFuture<Drawable>> iconFutures; List<ListenableFuture<Drawable>> iconFutures;
int extraItems = 0; int extraItems;
if (iconSet.size() > numIconsThatFit) { if (iconSet.size() > numIconsThatFit) {
// Reserve one space for the (+xx) circle. // Reserve one space for the (+xx) textview.
int numIconsToShow = numIconsThatFit - 1; int numIconsToShow = numIconsThatFit - 1;
if (numIconsToShow < 0) { if (numIconsToShow < 0) {
numIconsToShow = 0; numIconsToShow = 0;
@@ -146,6 +159,7 @@ public class CircularIconsPreference extends RestrictedPreference {
} else { } else {
// Fit exactly or with remaining space. // Fit exactly or with remaining space.
iconFutures = iconSet.getIcons(); iconFutures = iconSet.getIcons();
extraItems = 0;
} }
displayIconsWhenReady(iconFutures, extraItems); displayIconsWhenReady(iconFutures, extraItems);
@@ -158,33 +172,45 @@ public class CircularIconsPreference extends RestrictedPreference {
mPendingLoadIconsFuture.cancel(true); mPendingLoadIconsFuture.cancel(true);
} }
int numCircles = iconFutures.size() + (extraItems > 0 ? 1 : 0); // Rearrange child views until we have <numImages> ImageViews...
if (mIconContainer.getChildCount() > numCircles) { LayoutInflater inflater = LayoutInflater.from(getContext());
mIconContainer.removeViews(numCircles, mIconContainer.getChildCount() - numCircles); int numImages = iconFutures.size();
int numImageViews = getChildCount(mIconContainer, ImageView.class);
if (numImages > numImageViews) {
for (int i = 0; i < numImages - numImageViews; i++) {
ImageView imageView = (ImageView) inflater.inflate(
R.layout.preference_circular_icons_item, mIconContainer, false);
mIconContainer.addView(imageView, 0);
}
} else if (numImageViews > numImages) {
for (int i = 0; i < numImageViews - numImages; i++) {
mIconContainer.removeViewAt(0);
}
} }
for (int i = mIconContainer.getChildCount(); i < numCircles; i++) { // ... plus 0/1 TextViews at the end.
ImageView imageView = (ImageView) LayoutInflater.from(getContext()).inflate( if (extraItems > 0 && !(getLastChild(mIconContainer) instanceof TextView)) {
R.layout.preference_circular_icons_item, mIconContainer, false); // TODO: b/346551087 - Check TODO in preference_circular_icons_plus_item_background
mIconContainer.addView(imageView); TextView plusView = (TextView) inflater.inflate(
R.layout.preference_circular_icons_plus_item, mIconContainer, false);
mIconContainer.addView(plusView);
} else if (extraItems == 0 && (getLastChild(mIconContainer) instanceof TextView)) {
mIconContainer.removeViewAt(mIconContainer.getChildCount() - 1);
} }
// Set up placeholders and extra items indicator. // Set up placeholders and extra items indicator.
for (int i = 0; i < iconFutures.size(); i++) { for (int i = 0; i < numImages; i++) {
ImageView imageView = (ImageView) mIconContainer.getChildAt(i); ImageView imageView = (ImageView) mIconContainer.getChildAt(i);
// TODO: b/346551087 - proper color and shape, should be a gray circle. imageView.setImageDrawable(getPlaceholderImage(getContext()));
imageView.setImageDrawable(new ColorDrawable(Color.RED));
} }
if (extraItems > 0) { if (extraItems > 0) {
ImageView imageView = (ImageView) mIconContainer.getChildAt( TextView textView = (TextView) checkNotNull(getLastChild(mIconContainer));
mIconContainer.getChildCount() - 1); textView.setText(getContext().getString(R.string.zen_mode_plus_n_items, extraItems));
// TODO: b/346551087 - proper color and shape and number.
imageView.setImageDrawable(new ColorDrawable(Color.BLUE));
} }
// Display icons when all are ready (more consistent than randomly loading). // Display icons when all are ready (more consistent than randomly loading).
mPendingLoadIconsFuture = Futures.allAsList(iconFutures); mPendingLoadIconsFuture = Futures.allAsList(iconFutures);
FutureUtil.whenDone( FutureUtil.whenDone(
Futures.allAsList(iconFutures), mPendingLoadIconsFuture,
icons -> { icons -> {
checkState(mIconContainer != null); checkState(mIconContainer != null);
for (int i = 0; i < icons.size(); i++) { for (int i = 0; i < icons.size(); i++) {
@@ -194,15 +220,54 @@ public class CircularIconsPreference extends RestrictedPreference {
mUiExecutor); mUiExecutor);
} }
private static Drawable getPlaceholderImage(Context context) {
ShapeDrawable placeholder = new ShapeDrawable(new OvalShape());
placeholder.setTintList(Utils.getColorAttr(context,
com.android.internal.R.attr.materialColorSecondaryContainer));
return placeholder;
}
private static int getChildCount(ViewGroup parent, Class<? extends View> childClass) {
int count = 0;
for (int i = 0; i < parent.getChildCount(); i++) {
if (childClass.isInstance(parent.getChildAt(i))) {
count++;
}
}
return count;
}
@Nullable
private static View getLastChild(ViewGroup parent) {
if (parent.getChildCount() == 0) {
return null;
}
return parent.getChildAt(parent.getChildCount() - 1);
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE) @VisibleForTesting(otherwise = VisibleForTesting.NONE)
ImmutableList<ImageView> getIconViews() { List<Drawable> getIcons() {
if (mIconContainer == null) { if (mIconContainer == null) {
return ImmutableList.of(); return List.of();
} }
ImmutableList.Builder<ImageView> imageViews = new ImmutableList.Builder<>(); ArrayList<Drawable> drawables = new ArrayList<>();
for (int i = 0; i < mIconContainer.getChildCount(); i++) { for (int i = 0; i < getChildCount(mIconContainer, ImageView.class); i++) {
imageViews.add((ImageView) mIconContainer.getChildAt(i)); drawables.add(((ImageView) mIconContainer.getChildAt(i)).getDrawable());
}
return drawables;
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
@Nullable
String getPlusText() {
if (mIconContainer == null) {
return null;
}
View lastChild = getLastChild(mIconContainer);
if (lastChild instanceof TextView tv) {
return tv.getText() != null ? tv.getText().toString() : null;
} else {
return null;
} }
return imageViews.build();
} }
} }

View File

@@ -55,7 +55,7 @@ class IconUtil {
* Returns a variant of the supplied {@code icon} to be used as the header in the icon picker. * Returns a variant of the supplied {@code icon} to be used as the header in the icon picker.
* The inner icon is 48x48dp and it's contained into a circle of diameter 90dp. * The inner icon is 48x48dp and it's contained into a circle of diameter 90dp.
*/ */
static Drawable makeBigIconCircle(@NonNull Context context, Drawable icon) { static Drawable makeIconPickerHeader(@NonNull Context context, Drawable icon) {
return composeIconCircle( return composeIconCircle(
Utils.getColorAttr(context, Utils.getColorAttr(context,
com.android.internal.R.attr.materialColorSecondaryContainer), com.android.internal.R.attr.materialColorSecondaryContainer),
@@ -73,7 +73,7 @@ class IconUtil {
* The inner icon is 36x36dp and it's contained into a circle of diameter 54dp. It's also set up * The inner icon is 36x36dp and it's contained into a circle of diameter 54dp. It's also set up
* so that selection and pressed states are represented in the color. * so that selection and pressed states are represented in the color.
*/ */
static Drawable makeSmallIconCircle(@NonNull Context context, @DrawableRes int iconResId) { static Drawable makeIconPickerItem(@NonNull Context context, @DrawableRes int iconResId) {
return composeIconCircle( return composeIconCircle(
context.getColorStateList(R.color.modes_icon_picker_item_background), context.getColorStateList(R.color.modes_icon_picker_item_background),
context.getResources().getDimensionPixelSize( context.getResources().getDimensionPixelSize(
@@ -84,6 +84,24 @@ class IconUtil {
R.dimen.zen_mode_icon_list_item_icon_size)); R.dimen.zen_mode_icon_list_item_icon_size));
} }
/**
* Returns a variant of the supplied icon to be used in a {@link CircularIconsPreference}. The
* inner icon is 20x20 dp and it's contained in a circle of diameter 32dp, and is tinted
* with the "material secondary container" color combination.
*/
static Drawable makeSoundIcon(@NonNull Context context, @DrawableRes int iconResId) {
return composeIconCircle(
Utils.getColorAttr(context,
com.android.internal.R.attr.materialColorSecondaryContainer),
context.getResources().getDimensionPixelSize(
R.dimen.zen_mode_circular_icon_diameter),
checkNotNull(context.getDrawable(iconResId)),
Utils.getColorAttr(context,
com.android.internal.R.attr.materialColorOnSecondaryContainer),
context.getResources().getDimensionPixelSize(
R.dimen.zen_mode_circular_icon_inner_icon_size));
}
private static Drawable composeIconCircle(ColorStateList circleColor, @Px int circleDiameterPx, private static Drawable composeIconCircle(ColorStateList circleColor, @Px int circleDiameterPx,
Drawable icon, ColorStateList iconColor, @Px int iconSizePx) { Drawable icon, ColorStateList iconColor, @Px int iconSizePx) {
ShapeDrawable background = new ShapeDrawable(new OvalShape()); ShapeDrawable background = new ShapeDrawable(new OvalShape());
@@ -93,11 +111,11 @@ class IconUtil {
LayerDrawable layerDrawable = new LayerDrawable(new Drawable[] { background, foreground }); LayerDrawable layerDrawable = new LayerDrawable(new Drawable[] { background, foreground });
layerDrawable.setBounds(0, 0, circleDiameterPx, circleDiameterPx);
layerDrawable.setLayerSize(0, circleDiameterPx, circleDiameterPx); layerDrawable.setLayerSize(0, circleDiameterPx, circleDiameterPx);
layerDrawable.setLayerGravity(1, Gravity.CENTER); layerDrawable.setLayerGravity(1, Gravity.CENTER);
layerDrawable.setLayerSize(1, iconSizePx, iconSizePx); layerDrawable.setLayerSize(1, iconSizePx, iconSizePx);
layerDrawable.setBounds(0, 0, circleDiameterPx, circleDiameterPx);
return layerDrawable; return layerDrawable;
} }
} }

View File

@@ -64,7 +64,7 @@ class ZenModeIconPickerIconPreferenceController extends AbstractZenModePreferenc
FutureUtil.whenDone( FutureUtil.whenDone(
zenMode.getIcon(mContext, ZenIconLoader.getInstance()), zenMode.getIcon(mContext, ZenIconLoader.getInstance()),
icon -> mHeaderController.setIcon(IconUtil.makeBigIconCircle(mContext, icon)) icon -> mHeaderController.setIcon(IconUtil.makeIconPickerHeader(mContext, icon))
.done(/* rebindActions= */ false), .done(/* rebindActions= */ false),
mContext.getMainExecutor()); mContext.getMainExecutor());
} }

View File

@@ -156,7 +156,7 @@ class ZenModeIconPickerListPreferenceController extends AbstractZenModePreferenc
public void onBindViewHolder(@NonNull IconHolder holder, int position) { public void onBindViewHolder(@NonNull IconHolder holder, int position) {
IconOptionsProvider.IconInfo iconInfo = mIconResources.get(position); IconOptionsProvider.IconInfo iconInfo = mIconResources.get(position);
Drawable iconDrawable = mIconCache.computeIfAbsent(iconInfo, Drawable iconDrawable = mIconCache.computeIfAbsent(iconInfo,
info -> IconUtil.makeSmallIconCircle(mContext, info.resId())); info -> IconUtil.makeIconPickerItem(mContext, info.resId()));
holder.bindIcon(iconInfo, iconDrawable); holder.bindIcon(iconInfo, iconDrawable);
} }

View File

@@ -17,19 +17,44 @@
package com.android.settings.notification.modes; package com.android.settings.notification.modes;
import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL; import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_ALARMS;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_EVENTS;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_MEDIA;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REMINDERS;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_SYSTEM;
import android.content.Context; import android.content.Context;
import android.service.notification.ZenPolicy;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.preference.Preference; import androidx.preference.Preference;
import com.android.settingslib.notification.modes.ZenMode; import com.android.settingslib.notification.modes.ZenMode;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.Map;
/** /**
* Preference with a link and summary about what other sounds can break through the mode * Preference with a link and summary about what other sounds can break through the mode
*/ */
class ZenModeOtherLinkPreferenceController extends AbstractZenModePreferenceController { class ZenModeOtherLinkPreferenceController extends AbstractZenModePreferenceController {
// TODO: b/346551087 - Use proper icons
private static final ImmutableMap</* @PriorityCategory */ Integer, /* @DrawableRes */ Integer>
PRIORITIES_TO_ICONS = ImmutableMap.of(
PRIORITY_CATEGORY_ALARMS,
com.android.internal.R.drawable.ic_audio_alarm,
PRIORITY_CATEGORY_MEDIA,
com.android.settings.R.drawable.ic_media_stream,
PRIORITY_CATEGORY_SYSTEM,
com.android.settings.R.drawable.ic_settings_keyboards,
PRIORITY_CATEGORY_REMINDERS,
com.android.internal.R.drawable.ic_popup_reminder,
PRIORITY_CATEGORY_EVENTS,
com.android.internal.R.drawable.ic_zen_mode_type_schedule_calendar);
private final ZenModeSummaryHelper mSummaryHelper; private final ZenModeSummaryHelper mSummaryHelper;
public ZenModeOtherLinkPreferenceController(Context context, String key, public ZenModeOtherLinkPreferenceController(Context context, String key,
@@ -51,7 +76,17 @@ class ZenModeOtherLinkPreferenceController extends AbstractZenModePreferenceCont
zenMode.getId(), 0).toIntent()); zenMode.getId(), 0).toIntent());
preference.setSummary(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)); preference.setSummary(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode));
// TODO: b/346551087 - Show media icons ((CircularIconsPreference) preference).displayIcons(getSoundIcons(zenMode.getPolicy()));
((CircularIconsPreference) preference).displayIcons(CircularIconSet.EMPTY); }
private CircularIconSet<Integer> getSoundIcons(ZenPolicy policy) {
ImmutableList.Builder<Integer> icons = new ImmutableList.Builder<>();
for (Map.Entry<Integer, Integer> entry : PRIORITIES_TO_ICONS.entrySet()) {
if (policy.isCategoryAllowed(entry.getKey(), false)) {
icons.add(entry.getValue());
}
}
return new CircularIconSet<>(icons.build(),
iconResId -> IconUtil.makeSoundIcon(mContext, iconResId));
} }
} }

View File

@@ -56,6 +56,8 @@ import com.android.settings.R;
import com.android.settingslib.applications.ApplicationsState.AppEntry; import com.android.settingslib.applications.ApplicationsState.AppEntry;
import com.android.settingslib.notification.modes.ZenMode; import com.android.settingslib.notification.modes.ZenMode;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -85,14 +87,18 @@ class ZenModeSummaryHelper {
PRIORITY_CATEGORY_REPEAT_CALLERS, PRIORITY_CATEGORY_REPEAT_CALLERS,
}; };
static final ImmutableList</* @PriorityCategory */ Integer> OTHER_SOUND_CATEGORIES =
ImmutableList.of(
PRIORITY_CATEGORY_ALARMS,
PRIORITY_CATEGORY_MEDIA,
PRIORITY_CATEGORY_SYSTEM,
PRIORITY_CATEGORY_REMINDERS,
PRIORITY_CATEGORY_EVENTS);
String getOtherSoundCategoriesSummary(ZenMode zenMode) { String getOtherSoundCategoriesSummary(ZenMode zenMode) {
List<String> enabledCategories = getEnabledCategories( List<String> enabledCategories = getEnabledCategories(
zenMode.getPolicy(), zenMode.getPolicy(),
category -> PRIORITY_CATEGORY_ALARMS == category OTHER_SOUND_CATEGORIES::contains,
|| PRIORITY_CATEGORY_MEDIA == category
|| PRIORITY_CATEGORY_SYSTEM == category
|| PRIORITY_CATEGORY_REMINDERS == category
|| PRIORITY_CATEGORY_EVENTS == category,
true); true);
int numCategories = enabledCategories.size(); int numCategories = enabledCategories.size();
MessageFormat msgFormat = new MessageFormat( MessageFormat msgFormat = new MessageFormat(

View File

@@ -53,6 +53,36 @@ public class CircularIconSetTest {
when(mDrawableLoader.apply(anyInt())).thenReturn(new ColorDrawable(Color.BLACK)); when(mDrawableLoader.apply(anyInt())).thenReturn(new ColorDrawable(Color.BLACK));
} }
@Test
public void equals_sameItems_true() {
CircularIconSet<Integer> items1 = new CircularIconSet<>(ImmutableList.of(1, 2),
num -> new ColorDrawable(Color.BLUE));
CircularIconSet<Integer> items2 = new CircularIconSet<>(ImmutableList.of(1, 2),
num -> new ColorDrawable(Color.GREEN));
assertThat(items1.hasSameItemsAs(items2)).isTrue();
}
@Test
public void equals_differentTypes_false() {
CircularIconSet<Integer> items1 = new CircularIconSet<>(ImmutableList.of(1, 2),
num -> new ColorDrawable(Color.BLUE));
CircularIconSet<String> items2 = new CircularIconSet<>(ImmutableList.of("a", "b"),
str -> new ColorDrawable(Color.GREEN));
assertThat(items1.hasSameItemsAs(items2)).isFalse();
}
@Test
public void equals_differentItems_false() {
CircularIconSet<String> items1 = new CircularIconSet<>(ImmutableList.of("a", "b"),
str -> new ColorDrawable(Color.GREEN));
CircularIconSet<String> items2 = new CircularIconSet<>(ImmutableList.of("a", "b", "c"),
str -> new ColorDrawable(Color.GREEN));
assertThat(items1.hasSameItemsAs(items2)).isFalse();
}
@Test @Test
public void getIcons_loadsAllIcons() { public void getIcons_loadsAllIcons() {
CircularIconSet<Integer> set = new CircularIconSet<>(ImmutableList.of(1, 2, 3), CircularIconSet<Integer> set = new CircularIconSet<>(ImmutableList.of(1, 2, 3),

View File

@@ -19,16 +19,16 @@ package com.android.settings.notification.modes;
import static android.view.View.MeasureSpec.makeMeasureSpec; import static android.view.View.MeasureSpec.makeMeasureSpec;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.when;
import android.content.Context; import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.widget.ImageView;
import androidx.preference.PreferenceViewHolder; import androidx.preference.PreferenceViewHolder;
@@ -41,11 +41,11 @@ import com.google.common.util.concurrent.MoreExecutors;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment; import org.robolectric.RuntimeEnvironment;
import java.util.List;
import java.util.stream.IntStream; import java.util.stream.IntStream;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
@@ -68,20 +68,30 @@ public class CircularIconsPreferenceTest {
// Tests should call bindAndMeasureViewHolder() so that icons can be added. // Tests should call bindAndMeasureViewHolder() so that icons can be added.
Resources res = mContext.getResources(); Resources res = mContext.getResources();
mOneIconWidth = res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_size) mOneIconWidth = res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_diameter)
+ res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_margin_between); + res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_margin_between);
} }
private void bindAndMeasureViewHolder(int viewWidth) { private void bindAndMeasureViewHolder(int viewWidth) {
bindViewHolder();
measureViewHolder(viewWidth);
}
private void bindViewHolder() {
View preferenceView = LayoutInflater.from(mContext).inflate(mPreference.getLayoutResource(), View preferenceView = LayoutInflater.from(mContext).inflate(mPreference.getLayoutResource(),
null); null);
mIconContainer = checkNotNull(preferenceView.findViewById(R.id.circles_container)); mIconContainer = checkNotNull(preferenceView.findViewById(R.id.circles_container));
mIconContainer.measure(makeMeasureSpec(viewWidth, View.MeasureSpec.EXACTLY),
makeMeasureSpec(1000, View.MeasureSpec.EXACTLY));
PreferenceViewHolder holder = PreferenceViewHolder.createInstanceForTests(preferenceView); PreferenceViewHolder holder = PreferenceViewHolder.createInstanceForTests(preferenceView);
mPreference.onBindViewHolder(holder); mPreference.onBindViewHolder(holder);
} }
private void measureViewHolder(int viewWidth) {
checkState(mIconContainer != null, "Call bindViewHolder() first!");
mIconContainer.measure(makeMeasureSpec(viewWidth, View.MeasureSpec.EXACTLY),
makeMeasureSpec(1000, View.MeasureSpec.EXACTLY));
mIconContainer.getViewTreeObserver().dispatchOnGlobalLayout();
}
@Test @Test
public void displayIcons_loadsIcons() { public void displayIcons_loadsIcons() {
CircularIconSet<Integer> iconSet = new CircularIconSet<>(ImmutableList.of(1, 2), CircularIconSet<Integer> iconSet = new CircularIconSet<>(ImmutableList.of(1, 2),
@@ -90,13 +100,10 @@ public class CircularIconsPreferenceTest {
bindAndMeasureViewHolder(VIEW_WIDTH); bindAndMeasureViewHolder(VIEW_WIDTH);
mPreference.displayIcons(iconSet); mPreference.displayIcons(iconSet);
assertThat(mPreference.getIconViews()).hasSize(2); assertThat(mPreference.getIcons()).hasSize(2);
assertThat(mPreference.getIconViews().get(0).getDrawable()) assertThat(((ColorDrawable) mPreference.getIcons().get(0)).getColor()).isEqualTo(1);
.isInstanceOf(ColorDrawable.class); assertThat(((ColorDrawable) mPreference.getIcons().get(1)).getColor()).isEqualTo(2);
assertThat(((ColorDrawable) mPreference.getIconViews().get(0).getDrawable()).getColor()) assertThat(mPreference.getPlusText()).isNull();
.isEqualTo(1);
assertThat(((ColorDrawable) mPreference.getIconViews().get(1).getDrawable()).getColor())
.isEqualTo(2);
assertThat(mIconContainer.getVisibility()).isEqualTo(View.VISIBLE); assertThat(mIconContainer.getVisibility()).isEqualTo(View.VISIBLE);
} }
@@ -111,74 +118,81 @@ public class CircularIconsPreferenceTest {
assertThat(mIconContainer.getVisibility()).isEqualTo(View.GONE); assertThat(mIconContainer.getVisibility()).isEqualTo(View.GONE);
} }
@Test @Test
public void displayIcons_exactlyMaxIcons_loadsAllIcons() throws Exception { public void displayIcons_exactlyMaxIcons_loadsAllIcons() throws Exception {
int width = 300; int width = 300;
int fittingIcons = width / mOneIconWidth; int fittingCircles = width / mOneIconWidth;
CircularIconSet<Integer> iconSet = new CircularIconSet<>( CircularIconSet<Integer> iconSet = new CircularIconSet<>(
IntStream.range(0, fittingIcons).boxed().toList(), IntStream.range(0, fittingCircles).boxed().toList(),
ColorDrawable::new); ColorDrawable::new);
bindAndMeasureViewHolder(width); bindAndMeasureViewHolder(width);
mPreference.displayIcons(iconSet); mPreference.displayIcons(iconSet);
List<Drawable> displayedDrawables = mPreference.getIconViews().stream() assertThat(mPreference.getIcons()).hasSize(fittingCircles);
.map(ImageView::getDrawable).toList(); assertThat(mPreference.getIcons()).containsExactlyElementsIn(
assertThat(displayedDrawables).hasSize(fittingIcons);
assertThat(displayedDrawables).containsExactlyElementsIn(
Futures.allAsList(iconSet.getIcons()).get()).inOrder(); Futures.allAsList(iconSet.getIcons()).get()).inOrder();
assertThat(mPreference.getPlusText()).isNull();
} }
@Test @Test
public void displayIcons_tooManyIcons_loadsFirstNAndPlusIcon() throws Exception { public void displayIcons_tooManyIcons_loadsFirstNAndPlusIcon() throws Exception {
int width = 300; int width = 300;
int fittingIcons = width / mOneIconWidth; int fittingCircles = width / mOneIconWidth;
CircularIconSet<Integer> iconSet = new CircularIconSet<>( CircularIconSet<Integer> iconSet = new CircularIconSet<>(
IntStream.range(0, fittingIcons + 5).boxed().toList(), IntStream.range(0, fittingCircles + 5).boxed().toList(),
ColorDrawable::new); ColorDrawable::new);
bindAndMeasureViewHolder(width); bindAndMeasureViewHolder(width);
mPreference.displayIcons(iconSet); mPreference.displayIcons(iconSet);
List<Drawable> displayedDrawables = mPreference.getIconViews().stream() // N-1 icons, plus (+6) text.
.map(ImageView::getDrawable).toList(); assertThat(mPreference.getIcons()).hasSize(fittingCircles - 1);
assertThat(displayedDrawables).hasSize(fittingIcons); assertThat(mPreference.getIcons()).containsExactlyElementsIn(
// N-1 are actual icons, Nth icon is (+xx). Futures.allAsList(iconSet.getIcons(fittingCircles - 1)).get())
assertThat(displayedDrawables.stream().limit(fittingIcons - 1).toList())
.containsExactlyElementsIn(
Futures.allAsList(iconSet.getIcons(fittingIcons - 1)).get())
.inOrder(); .inOrder();
// TODO: b/346551087 - Correctly verify the plus-6 icon, once we generate it properly. assertThat(mPreference.getPlusText()).isEqualTo("+6");
assertThat(((ColorDrawable) displayedDrawables.get(
displayedDrawables.size() - 1)).getColor()).isEqualTo(Color.BLUE);
} }
@Test @Test
public void displayIcons_teenyTinySpace_showsPlusIcon_noCrash() { public void displayIcons_teenyTinySpace_showsPlusIcon_noCrash() {
int width = 1;
CircularIconSet<Integer> iconSet = new CircularIconSet<>(ImmutableList.of(1, 2), CircularIconSet<Integer> iconSet = new CircularIconSet<>(ImmutableList.of(1, 2),
ColorDrawable::new); ColorDrawable::new);
bindAndMeasureViewHolder(width); bindAndMeasureViewHolder(1);
mPreference.displayIcons(iconSet); mPreference.displayIcons(iconSet);
assertThat(mPreference.getIconViews()).hasSize(1); assertThat(mPreference.getIcons()).isEmpty();
// TODO: b/346551087 - Correctly verify the plus-2 icon, once we generate it properly. assertThat(mPreference.getPlusText()).isEqualTo("+2");
assertThat(((ColorDrawable) mPreference.getIconViews().get(0).getDrawable()).getColor())
.isEqualTo(Color.BLUE);
} }
@Test @Test
public void displayIcons_beforeBind_loadsIconsOnBind() { public void displayIcons_beforeBind_loadsIconsOnBindAndMeasure() {
CircularIconSet<Integer> iconSet = new CircularIconSet<>(ImmutableList.of(1, 2, 3), CircularIconSet<Integer> iconSet = new CircularIconSet<>(ImmutableList.of(1, 2, 3),
ColorDrawable::new); ColorDrawable::new);
mPreference.displayIcons(iconSet); mPreference.displayIcons(iconSet);
assertThat(mPreference.getIconViews()).isEmpty(); assertThat(mPreference.getIcons()).isEmpty(); // Hold...
bindAndMeasureViewHolder(VIEW_WIDTH); bindViewHolder();
assertThat(mPreference.getIconViews()).hasSize(3); assertThat(mPreference.getIcons()).isEmpty(); // Hooooold...
measureViewHolder(VIEW_WIDTH);
assertThat(mPreference.getIcons()).hasSize(3);
}
@Test
public void displayIcons_beforeMeasure_loadsIconsOnMeasure() {
CircularIconSet<Integer> iconSet = new CircularIconSet<>(ImmutableList.of(1, 2, 3),
ColorDrawable::new);
bindViewHolder();
mPreference.displayIcons(iconSet);
assertThat(mPreference.getIcons()).isEmpty();
measureViewHolder(VIEW_WIDTH);
assertThat(mPreference.getIcons()).hasSize(3);
} }
@Test @Test
@@ -192,10 +206,24 @@ public class CircularIconsPreferenceTest {
bindAndMeasureViewHolder(VIEW_WIDTH); bindAndMeasureViewHolder(VIEW_WIDTH);
mPreference.displayIcons(threeIcons); mPreference.displayIcons(threeIcons);
assertThat(mPreference.getIconViews()).hasSize(3); assertThat(mPreference.getIcons()).hasSize(3);
mPreference.displayIcons(twoIcons); mPreference.displayIcons(twoIcons);
assertThat(mPreference.getIconViews()).hasSize(2); assertThat(mPreference.getIcons()).hasSize(2);
mPreference.displayIcons(fourIcons); mPreference.displayIcons(fourIcons);
assertThat(mPreference.getIconViews()).hasSize(4); assertThat(mPreference.getIcons()).hasSize(4);
}
@Test
public void displayIcons_sameSet_doesNotReloadIcons() {
CircularIconSet<Integer> one = new CircularIconSet<>(ImmutableList.of(1, 2, 3),
ColorDrawable::new);
CircularIconSet<Integer> same = Mockito.spy(new CircularIconSet<>(ImmutableList.of(1, 2, 3),
ColorDrawable::new));
when(same.getIcons()).thenThrow(new RuntimeException("Shouldn't be called!"));
bindAndMeasureViewHolder(VIEW_WIDTH);
mPreference.displayIcons(one);
mPreference.displayIcons(same); // if no exception, wasn't called.
} }
} }

View File

@@ -259,7 +259,7 @@ public final class ZenModeAppsLinkPreferenceControllerTest {
appEntries.add(createAppEntry("test2", mContext.getUserId())); appEntries.add(createAppEntry("test2", mContext.getUserId()));
mController.mAppSessionCallbacks.onRebuildComplete(appEntries); mController.mAppSessionCallbacks.onRebuildComplete(appEntries);
assertThat(mPreference.getIconViews()).hasSize(2); assertThat(mPreference.getIcons()).hasSize(2);
} }
@Test @Test

View File

@@ -17,7 +17,7 @@
package com.android.settings.notification.modes; package com.android.settings.notification.modes;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@@ -25,8 +25,10 @@ import android.app.Flags;
import android.content.Context; import android.content.Context;
import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule; import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.ZenPolicy;
import com.android.settingslib.notification.modes.TestModeBuilder; import com.android.settingslib.notification.modes.TestModeBuilder;
import com.android.settingslib.notification.modes.ZenMode;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
@@ -60,13 +62,40 @@ public final class ZenModeOtherLinkPreferenceControllerTest {
} }
@Test @Test
@EnableFlags(Flags.FLAG_MODES_UI) public void updateState_loadsSummary() {
public void testHasSummary() {
CircularIconsPreference pref = mock(CircularIconsPreference.class); CircularIconsPreference pref = mock(CircularIconsPreference.class);
mController.updateZenMode(pref, TestModeBuilder.EXAMPLE); mController.updateZenMode(pref, TestModeBuilder.EXAMPLE);
verify(pref).setSummary(any()); verify(pref).setSummary(any());
verify(pref).displayIcons(eq(CircularIconSet.EMPTY)); }
@Test
public void updateState_loadsIcons() {
CircularIconsPreference pref = mock(CircularIconsPreference.class);
ZenMode mode = new TestModeBuilder()
.setZenPolicy(new ZenPolicy.Builder()
.disallowAllSounds()
.allowMedia(true)
.allowSystem(true)
.allowReminders(true)
.build())
.build();
mController.updateState(pref, mode);
verify(pref).displayIcons(argThat(iconSet -> iconSet.size() == 3));
}
@Test
public void updateState_loadsAllIcons() {
CircularIconsPreference pref = mock(CircularIconsPreference.class);
ZenMode mode = new TestModeBuilder()
.setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
.build();
mController.updateState(pref, mode);
verify(pref).displayIcons(argThat(iconSet ->
iconSet.size() == ZenModeSummaryHelper.OTHER_SOUND_CATEGORIES.size()));
} }
} }