Merge "Show icons for allowed sounds" into main

This commit is contained in:
Matías Hernández
2024-07-19 17:06:27 +00:00
committed by Android (Google) Code Review
16 changed files with 389 additions and 105 deletions

View File

@@ -22,6 +22,7 @@ import android.graphics.drawable.Drawable;
import androidx.annotation.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
@@ -61,6 +62,15 @@ class CircularIconSet<T> {
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() {
return mItems.size();
}

View File

@@ -21,15 +21,17 @@ import static com.google.common.base.Preconditions.checkState;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@@ -37,11 +39,12 @@ import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
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.ListenableFuture;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
@@ -50,8 +53,9 @@ public class CircularIconsPreference extends RestrictedPreference {
private Executor mUiExecutor;
@Nullable private LinearLayout mIconContainer;
@Nullable private CircularIconSet<?> mPendingIconSet;
@Nullable private ListenableFuture<?> mPendingLoadIconsFuture;
@Nullable private CircularIconSet<?> mIconSet;
@Nullable private CircularIconSet<?> mPendingDisplayIconSet;
@Nullable private ListenableFuture<List<Drawable>> mPendingLoadIconsFuture;
public CircularIconsPreference(Context context) {
super(context);
@@ -94,17 +98,25 @@ public class CircularIconsPreference extends RestrictedPreference {
}
private void displayIconsIfPending() {
CircularIconSet<?> pendingIconSet = mPendingIconSet;
CircularIconSet<?> pendingIconSet = mPendingDisplayIconSet;
if (pendingIconSet != null) {
mPendingIconSet = null;
displayIcons(pendingIconSet);
mPendingDisplayIconSet = null;
displayIconsInternal(pendingIconSet);
}
}
void displayIcons(CircularIconSet<?> iconSet) {
if (mIconSet != null && mIconSet.hasSameItemsAs(iconSet)) {
return;
}
mIconSet = iconSet;
displayIconsInternal(iconSet);
}
void displayIconsInternal(CircularIconSet<?> iconSet) {
if (mIconContainer == null) {
// Too soon, wait for bind.
mPendingIconSet = iconSet;
mPendingDisplayIconSet = iconSet;
return;
}
mIconContainer.setVisibility(iconSet.size() != 0 ? View.VISIBLE : View.GONE);
@@ -113,30 +125,31 @@ public class CircularIconsPreference extends RestrictedPreference {
}
if (mIconContainer.getMeasuredWidth() == 0) {
// Too soon, wait for first measure to know width.
mPendingIconSet = iconSet;
ViewTreeObserver vto = mIconContainer.getViewTreeObserver();
vto.addOnGlobalLayoutListener(() ->
mPendingDisplayIconSet = iconSet;
mIconContainer.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
vto.removeOnGlobalLayoutListener(this);
checkNotNull(mIconContainer).getViewTreeObserver()
.removeOnGlobalLayoutListener(this);
displayIconsIfPending();
}
});
}
);
return;
}
mIconContainer.setVisibility(View.VISIBLE);
Resources res = getContext().getResources();
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);
int numIconsThatFit = availableSpace / iconHorizontalSpace;
List<ListenableFuture<Drawable>> iconFutures;
int extraItems = 0;
int extraItems;
if (iconSet.size() > numIconsThatFit) {
// Reserve one space for the (+xx) circle.
// Reserve one space for the (+xx) textview.
int numIconsToShow = numIconsThatFit - 1;
if (numIconsToShow < 0) {
numIconsToShow = 0;
@@ -146,6 +159,7 @@ public class CircularIconsPreference extends RestrictedPreference {
} else {
// Fit exactly or with remaining space.
iconFutures = iconSet.getIcons();
extraItems = 0;
}
displayIconsWhenReady(iconFutures, extraItems);
@@ -158,33 +172,45 @@ public class CircularIconsPreference extends RestrictedPreference {
mPendingLoadIconsFuture.cancel(true);
}
int numCircles = iconFutures.size() + (extraItems > 0 ? 1 : 0);
if (mIconContainer.getChildCount() > numCircles) {
mIconContainer.removeViews(numCircles, mIconContainer.getChildCount() - numCircles);
// Rearrange child views until we have <numImages> ImageViews...
LayoutInflater inflater = LayoutInflater.from(getContext());
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++) {
ImageView imageView = (ImageView) LayoutInflater.from(getContext()).inflate(
R.layout.preference_circular_icons_item, mIconContainer, false);
mIconContainer.addView(imageView);
// ... plus 0/1 TextViews at the end.
if (extraItems > 0 && !(getLastChild(mIconContainer) instanceof TextView)) {
// TODO: b/346551087 - Check TODO in preference_circular_icons_plus_item_background
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.
for (int i = 0; i < iconFutures.size(); i++) {
for (int i = 0; i < numImages; i++) {
ImageView imageView = (ImageView) mIconContainer.getChildAt(i);
// TODO: b/346551087 - proper color and shape, should be a gray circle.
imageView.setImageDrawable(new ColorDrawable(Color.RED));
imageView.setImageDrawable(getPlaceholderImage(getContext()));
}
if (extraItems > 0) {
ImageView imageView = (ImageView) mIconContainer.getChildAt(
mIconContainer.getChildCount() - 1);
// TODO: b/346551087 - proper color and shape and number.
imageView.setImageDrawable(new ColorDrawable(Color.BLUE));
TextView textView = (TextView) checkNotNull(getLastChild(mIconContainer));
textView.setText(getContext().getString(R.string.zen_mode_plus_n_items, extraItems));
}
// Display icons when all are ready (more consistent than randomly loading).
mPendingLoadIconsFuture = Futures.allAsList(iconFutures);
FutureUtil.whenDone(
Futures.allAsList(iconFutures),
mPendingLoadIconsFuture,
icons -> {
checkState(mIconContainer != null);
for (int i = 0; i < icons.size(); i++) {
@@ -194,15 +220,54 @@ public class CircularIconsPreference extends RestrictedPreference {
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)
ImmutableList<ImageView> getIconViews() {
List<Drawable> getIcons() {
if (mIconContainer == null) {
return ImmutableList.of();
return List.of();
}
ImmutableList.Builder<ImageView> imageViews = new ImmutableList.Builder<>();
for (int i = 0; i < mIconContainer.getChildCount(); i++) {
imageViews.add((ImageView) mIconContainer.getChildAt(i));
ArrayList<Drawable> drawables = new ArrayList<>();
for (int i = 0; i < getChildCount(mIconContainer, ImageView.class); 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.
* 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(
Utils.getColorAttr(context,
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
* 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(
context.getColorStateList(R.color.modes_icon_picker_item_background),
context.getResources().getDimensionPixelSize(
@@ -84,6 +84,24 @@ class IconUtil {
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,
Drawable icon, ColorStateList iconColor, @Px int iconSizePx) {
ShapeDrawable background = new ShapeDrawable(new OvalShape());
@@ -93,11 +111,11 @@ class IconUtil {
LayerDrawable layerDrawable = new LayerDrawable(new Drawable[] { background, foreground });
layerDrawable.setBounds(0, 0, circleDiameterPx, circleDiameterPx);
layerDrawable.setLayerSize(0, circleDiameterPx, circleDiameterPx);
layerDrawable.setLayerGravity(1, Gravity.CENTER);
layerDrawable.setLayerSize(1, iconSizePx, iconSizePx);
layerDrawable.setBounds(0, 0, circleDiameterPx, circleDiameterPx);
return layerDrawable;
}
}

View File

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

View File

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

View File

@@ -17,19 +17,44 @@
package com.android.settings.notification.modes;
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.service.notification.ZenPolicy;
import androidx.annotation.NonNull;
import androidx.preference.Preference;
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
*/
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;
public ZenModeOtherLinkPreferenceController(Context context, String key,
@@ -51,7 +76,17 @@ class ZenModeOtherLinkPreferenceController extends AbstractZenModePreferenceCont
zenMode.getId(), 0).toIntent());
preference.setSummary(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode));
// TODO: b/346551087 - Show media icons
((CircularIconsPreference) preference).displayIcons(CircularIconSet.EMPTY);
((CircularIconsPreference) preference).displayIcons(getSoundIcons(zenMode.getPolicy()));
}
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.notification.modes.ZenMode;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -85,14 +87,18 @@ class ZenModeSummaryHelper {
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) {
List<String> enabledCategories = getEnabledCategories(
zenMode.getPolicy(),
category -> PRIORITY_CATEGORY_ALARMS == category
|| PRIORITY_CATEGORY_MEDIA == category
|| PRIORITY_CATEGORY_SYSTEM == category
|| PRIORITY_CATEGORY_REMINDERS == category
|| PRIORITY_CATEGORY_EVENTS == category,
OTHER_SOUND_CATEGORIES::contains,
true);
int numCategories = enabledCategories.size();
MessageFormat msgFormat = new MessageFormat(