Merge changes Ibd89a826,I7e788b9b into main
* changes: Fixes to CircularIconsPreference & friends Update top of modes page
This commit is contained in:
committed by
Android (Google) Code Review
commit
e6198dea95
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
package com.android.settings.notification.modes;
|
||||
|
||||
import android.app.Flags;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.dashboard.DashboardFragment;
|
||||
import com.android.settings.widget.EntityHeaderController;
|
||||
import com.android.settingslib.notification.modes.ZenIconLoader;
|
||||
import com.android.settingslib.notification.modes.ZenMode;
|
||||
import com.android.settingslib.widget.LayoutPreference;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
abstract class AbstractZenModeHeaderController extends AbstractZenModePreferenceController {
|
||||
|
||||
private final DashboardFragment mFragment;
|
||||
private EntityHeaderController mHeaderController;
|
||||
|
||||
AbstractZenModeHeaderController(
|
||||
@NonNull Context context,
|
||||
@NonNull String key,
|
||||
@NonNull DashboardFragment fragment) {
|
||||
super(context, key);
|
||||
mFragment = fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return Flags.modesApi() && Flags.modesUi();
|
||||
}
|
||||
|
||||
protected void updateIcon(Preference preference, @NonNull ZenMode zenMode, int iconSizePx,
|
||||
Function<Drawable, Drawable> modeIconStylist,
|
||||
@Nullable Consumer<ImageView> iconViewCustomizer) {
|
||||
if (mFragment == null) {
|
||||
return;
|
||||
}
|
||||
preference.setSelectable(false);
|
||||
|
||||
if (mHeaderController == null) {
|
||||
final LayoutPreference pref = (LayoutPreference) preference;
|
||||
mHeaderController = EntityHeaderController.newInstance(
|
||||
mFragment.getActivity(),
|
||||
mFragment,
|
||||
pref.findViewById(R.id.entity_header));
|
||||
}
|
||||
|
||||
ImageView iconView = ((LayoutPreference) preference).findViewById(R.id.entity_header_icon);
|
||||
if (iconView != null) {
|
||||
if (iconViewCustomizer != null) {
|
||||
iconViewCustomizer.accept(iconView);
|
||||
}
|
||||
ViewGroup.LayoutParams layoutParams = iconView.getLayoutParams();
|
||||
if (layoutParams.width != iconSizePx || layoutParams.height != iconSizePx) {
|
||||
layoutParams.width = iconSizePx;
|
||||
layoutParams.height = iconSizePx;
|
||||
iconView.setLayoutParams(layoutParams);
|
||||
}
|
||||
}
|
||||
|
||||
FutureUtil.whenDone(
|
||||
zenMode.getIcon(mContext, ZenIconLoader.getInstance()),
|
||||
icon -> mHeaderController
|
||||
.setIcon(modeIconStylist.apply(icon))
|
||||
.done(/* rebindActions= */ false),
|
||||
mContext.getMainExecutor());
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,10 @@
|
||||
package com.android.settings.notification.modes;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
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;
|
||||
@@ -39,13 +36,12 @@ import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settingslib.RestrictedPreference;
|
||||
import com.android.settingslib.Utils;
|
||||
|
||||
import com.google.common.base.Equivalence;
|
||||
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;
|
||||
|
||||
@@ -53,12 +49,14 @@ public class CircularIconsPreference extends RestrictedPreference {
|
||||
|
||||
private static final float DISABLED_ITEM_ALPHA = 0.3f;
|
||||
|
||||
private Executor mUiExecutor;
|
||||
@Nullable private LinearLayout mIconContainer;
|
||||
record LoadedIcons(ImmutableList<Drawable> icons, int extraItems) { }
|
||||
|
||||
private Executor mUiExecutor;
|
||||
|
||||
// Chronologically, fields will be set top-to-bottom.
|
||||
@Nullable private CircularIconSet<?> mIconSet;
|
||||
@Nullable private CircularIconSet<?> mPendingDisplayIconSet;
|
||||
@Nullable private ListenableFuture<List<Drawable>> mPendingLoadIconsFuture;
|
||||
@Nullable private LoadedIcons mLoadedIcons;
|
||||
|
||||
public CircularIconsPreference(Context context) {
|
||||
super(context);
|
||||
@@ -92,30 +90,6 @@ public class CircularIconsPreference extends RestrictedPreference {
|
||||
setLayoutResource(R.layout.preference_circular_icons);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
|
||||
mIconContainer = checkNotNull((LinearLayout) holder.findViewById(R.id.circles_container));
|
||||
displayIconsIfPending();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
super.setEnabled(enabled);
|
||||
if (mIconContainer != null) {
|
||||
applyEnabledToIcons(mIconContainer, enabled);
|
||||
}
|
||||
}
|
||||
|
||||
private void displayIconsIfPending() {
|
||||
CircularIconSet<?> pendingIconSet = mPendingDisplayIconSet;
|
||||
if (pendingIconSet != null) {
|
||||
mPendingDisplayIconSet = null;
|
||||
displayIconsInternal(pendingIconSet);
|
||||
}
|
||||
}
|
||||
|
||||
<T> void displayIcons(CircularIconSet<T> iconSet) {
|
||||
displayIcons(iconSet, null);
|
||||
}
|
||||
@@ -125,38 +99,55 @@ public class CircularIconsPreference extends RestrictedPreference {
|
||||
return;
|
||||
}
|
||||
mIconSet = iconSet;
|
||||
displayIconsInternal(iconSet);
|
||||
|
||||
mLoadedIcons = null;
|
||||
if (mPendingLoadIconsFuture != null) {
|
||||
mPendingLoadIconsFuture.cancel(true);
|
||||
mPendingLoadIconsFuture = null;
|
||||
}
|
||||
|
||||
notifyChanged();
|
||||
}
|
||||
|
||||
void displayIconsInternal(CircularIconSet<?> iconSet) {
|
||||
if (mIconContainer == null) {
|
||||
// Too soon, wait for bind.
|
||||
mPendingDisplayIconSet = iconSet;
|
||||
return;
|
||||
}
|
||||
mIconContainer.setVisibility(iconSet.size() != 0 ? View.VISIBLE : View.GONE);
|
||||
if (iconSet.size() == 0) {
|
||||
return;
|
||||
}
|
||||
if (mIconContainer.getMeasuredWidth() == 0) {
|
||||
// Too soon, wait for first measure to know width.
|
||||
mPendingDisplayIconSet = iconSet;
|
||||
mIconContainer.getViewTreeObserver().addOnGlobalLayoutListener(
|
||||
new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
checkNotNull(mIconContainer).getViewTreeObserver()
|
||||
.removeOnGlobalLayoutListener(this);
|
||||
displayIconsIfPending();
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
|
||||
mIconContainer.setVisibility(View.VISIBLE);
|
||||
LinearLayout iconContainer = checkNotNull(
|
||||
(LinearLayout) holder.findViewById(R.id.circles_container));
|
||||
bindIconContainer(iconContainer);
|
||||
}
|
||||
|
||||
private void bindIconContainer(LinearLayout container) {
|
||||
if (mLoadedIcons != null) {
|
||||
// We have the icons ready to display already, show them.
|
||||
setDrawables(container, mLoadedIcons);
|
||||
} else if (mIconSet != null) {
|
||||
// We know what icons we want, but haven't yet loaded them.
|
||||
if (mIconSet.size() == 0) {
|
||||
container.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
container.setVisibility(View.VISIBLE);
|
||||
if (container.getMeasuredWidth() != 0) {
|
||||
startLoadingIcons(container, mIconSet);
|
||||
} else {
|
||||
container.getViewTreeObserver().addOnGlobalLayoutListener(
|
||||
new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
container.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
startLoadingIcons(container, mIconSet);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startLoadingIcons(LinearLayout container, CircularIconSet<?> iconSet) {
|
||||
Resources res = getContext().getResources();
|
||||
int availableSpace = mIconContainer.getMeasuredWidth();
|
||||
int availableSpace = container.getMeasuredWidth();
|
||||
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;
|
||||
@@ -177,79 +168,60 @@ public class CircularIconsPreference extends RestrictedPreference {
|
||||
extraItems = 0;
|
||||
}
|
||||
|
||||
displayIconsWhenReady(iconFutures, extraItems);
|
||||
}
|
||||
|
||||
private void displayIconsWhenReady(List<ListenableFuture<Drawable>> iconFutures,
|
||||
int extraItems) {
|
||||
checkState(mIconContainer != null);
|
||||
if (mPendingLoadIconsFuture != null) {
|
||||
mPendingLoadIconsFuture.cancel(true);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
// ... plus 0/1 TextViews at the end.
|
||||
if (extraItems > 0 && !(getLastChild(mIconContainer) instanceof TextView)) {
|
||||
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 < numImages; i++) {
|
||||
ImageView imageView = (ImageView) mIconContainer.getChildAt(i);
|
||||
imageView.setImageDrawable(getPlaceholderImage(getContext()));
|
||||
}
|
||||
if (extraItems > 0) {
|
||||
TextView textView = (TextView) checkNotNull(getLastChild(mIconContainer));
|
||||
textView.setText(getContext().getString(R.string.zen_mode_plus_n_items, extraItems));
|
||||
}
|
||||
|
||||
applyEnabledToIcons(mIconContainer, isEnabled());
|
||||
|
||||
// Display icons when all are ready (more consistent than randomly loading).
|
||||
mPendingLoadIconsFuture = Futures.allAsList(iconFutures);
|
||||
FutureUtil.whenDone(
|
||||
mPendingLoadIconsFuture,
|
||||
icons -> {
|
||||
checkState(mIconContainer != null);
|
||||
for (int i = 0; i < icons.size(); i++) {
|
||||
((ImageView) mIconContainer.getChildAt(i)).setImageDrawable(icons.get(i));
|
||||
}
|
||||
mLoadedIcons = new LoadedIcons(ImmutableList.copyOf(icons), extraItems);
|
||||
notifyChanged(); // So that view is rebound and icons actually shown.
|
||||
},
|
||||
mUiExecutor);
|
||||
}
|
||||
|
||||
private void applyEnabledToIcons(ViewGroup container, boolean enabled) {
|
||||
private void setDrawables(LinearLayout container, LoadedIcons loadedIcons) {
|
||||
// Rearrange child views until we have <numImages> ImageViews...
|
||||
LayoutInflater inflater = LayoutInflater.from(getContext());
|
||||
int numImages = loadedIcons.icons.size();
|
||||
int numImageViews = getChildCount(container, ImageView.class);
|
||||
if (numImages > numImageViews) {
|
||||
for (int i = 0; i < numImages - numImageViews; i++) {
|
||||
ImageView imageView = (ImageView) inflater.inflate(
|
||||
R.layout.preference_circular_icons_item, container, false);
|
||||
container.addView(imageView, 0);
|
||||
}
|
||||
} else if (numImageViews > numImages) {
|
||||
for (int i = 0; i < numImageViews - numImages; i++) {
|
||||
container.removeViewAt(0);
|
||||
}
|
||||
}
|
||||
// ... plus 0/1 TextViews at the end.
|
||||
if (loadedIcons.extraItems > 0 && !(getLastChild(container) instanceof TextView)) {
|
||||
TextView plusView = (TextView) inflater.inflate(
|
||||
R.layout.preference_circular_icons_plus_item, container, false);
|
||||
container.addView(plusView);
|
||||
} else if (loadedIcons.extraItems == 0 && (getLastChild(container) instanceof TextView)) {
|
||||
container.removeViewAt(container.getChildCount() - 1);
|
||||
}
|
||||
|
||||
// Show images (and +n if needed).
|
||||
for (int i = 0; i < numImages; i++) {
|
||||
ImageView imageView = (ImageView) container.getChildAt(i);
|
||||
imageView.setImageDrawable(loadedIcons.icons.get(i));
|
||||
}
|
||||
if (loadedIcons.extraItems > 0) {
|
||||
TextView textView = (TextView) checkNotNull(getLastChild(container));
|
||||
textView.setText(getContext().getString(R.string.zen_mode_plus_n_items,
|
||||
loadedIcons.extraItems));
|
||||
}
|
||||
|
||||
// Apply enabled/disabled style.
|
||||
for (int i = 0; i < container.getChildCount(); i++) {
|
||||
View child = container.getChildAt(i);
|
||||
child.setAlpha(enabled ? 1.0f : DISABLED_ITEM_ALPHA);
|
||||
child.setAlpha(isEnabled() ? 1.0f : DISABLED_ITEM_ALPHA);
|
||||
}
|
||||
}
|
||||
|
||||
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++) {
|
||||
@@ -268,41 +240,9 @@ public class CircularIconsPreference extends RestrictedPreference {
|
||||
return parent.getChildAt(parent.getChildCount() - 1);
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
List<View> getViews() {
|
||||
if (mIconContainer == null) {
|
||||
return List.of();
|
||||
}
|
||||
ArrayList<View> views = new ArrayList<>();
|
||||
for (int i = 0; i < mIconContainer.getChildCount(); i++) {
|
||||
views.add(mIconContainer.getChildAt(i));
|
||||
}
|
||||
return views;
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
List<Drawable> getIcons() {
|
||||
if (mIconContainer == null) {
|
||||
return List.of();
|
||||
}
|
||||
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;
|
||||
}
|
||||
LoadedIcons getLoadedIcons() {
|
||||
return mLoadedIcons;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,9 +63,27 @@ class IconUtil {
|
||||
return icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a variant of the supplied mode icon to be used as the header in the mode page. The
|
||||
* inner icon is 64x64 dp and it's contained in a 12-sided-cookie of 136dp diameter. It's
|
||||
* tinted with the "material secondary" color combination and the "selected" color variant
|
||||
* should be used for modes currently active.
|
||||
*/
|
||||
static Drawable makeModeHeader(@NonNull Context context, Drawable modeIcon) {
|
||||
return composeIcons(
|
||||
checkNotNull(context.getDrawable(R.drawable.ic_zen_mode_icon_cookie)),
|
||||
context.getColorStateList(R.color.modes_icon_selectable_background),
|
||||
context.getResources().getDimensionPixelSize(
|
||||
R.dimen.zen_mode_header_size),
|
||||
modeIcon,
|
||||
context.getColorStateList(R.color.modes_icon_selectable_icon),
|
||||
context.getResources().getDimensionPixelSize(
|
||||
R.dimen.zen_mode_header_inner_icon_size));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 in a circle of diameter 90dp.
|
||||
*/
|
||||
static Drawable makeIconPickerHeader(@NonNull Context context, Drawable icon) {
|
||||
return composeIconCircle(
|
||||
@@ -82,16 +100,16 @@ class IconUtil {
|
||||
|
||||
/**
|
||||
* Returns a variant of the supplied {@code icon} to be used as an option in the icon picker.
|
||||
* 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 in a circle of diameter 54dp. It's also set up
|
||||
* so that selection and pressed states are represented in the color.
|
||||
*/
|
||||
static Drawable makeIconPickerItem(@NonNull Context context, @DrawableRes int iconResId) {
|
||||
return composeIconCircle(
|
||||
context.getColorStateList(R.color.modes_icon_picker_item_background),
|
||||
context.getColorStateList(R.color.modes_icon_selectable_background),
|
||||
context.getResources().getDimensionPixelSize(
|
||||
R.dimen.zen_mode_icon_list_item_circle_diameter),
|
||||
checkNotNull(context.getDrawable(iconResId)),
|
||||
context.getColorStateList(R.color.modes_icon_picker_item_icon),
|
||||
context.getColorStateList(R.color.modes_icon_selectable_icon),
|
||||
context.getResources().getDimensionPixelSize(
|
||||
R.dimen.zen_mode_icon_list_item_icon_size));
|
||||
}
|
||||
@@ -164,18 +182,24 @@ class IconUtil {
|
||||
|
||||
private static Drawable composeIconCircle(ColorStateList circleColor, @Px int circleDiameterPx,
|
||||
Drawable icon, ColorStateList iconColor, @Px int iconSizePx) {
|
||||
ShapeDrawable background = new ShapeDrawable(new OvalShape());
|
||||
background.setTintList(circleColor);
|
||||
return composeIcons(new ShapeDrawable(new OvalShape()), circleColor, circleDiameterPx, icon,
|
||||
iconColor, iconSizePx);
|
||||
}
|
||||
|
||||
private static Drawable composeIcons(Drawable outer, ColorStateList outerColor,
|
||||
@Px int outerSizePx, Drawable icon, ColorStateList iconColor, @Px int iconSizePx) {
|
||||
Drawable background = checkNotNull(outer.getConstantState()).newDrawable().mutate();
|
||||
background.setTintList(outerColor);
|
||||
Drawable foreground = checkNotNull(icon.getConstantState()).newDrawable().mutate();
|
||||
foreground.setTintList(iconColor);
|
||||
|
||||
LayerDrawable layerDrawable = new LayerDrawable(new Drawable[] { background, foreground });
|
||||
|
||||
layerDrawable.setLayerSize(0, circleDiameterPx, circleDiameterPx);
|
||||
layerDrawable.setLayerSize(0, outerSizePx, outerSizePx);
|
||||
layerDrawable.setLayerGravity(1, Gravity.CENTER);
|
||||
layerDrawable.setLayerSize(1, iconSizePx, iconSizePx);
|
||||
|
||||
layerDrawable.setBounds(0, 0, circleDiameterPx, circleDiameterPx);
|
||||
layerDrawable.setBounds(0, 0, outerSizePx, outerSizePx);
|
||||
return layerDrawable;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package com.android.settings.notification.modes;
|
||||
|
||||
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settingslib.widget.TopIntroPreference;
|
||||
|
||||
public class ZenModeBlurbPreference extends TopIntroPreference {
|
||||
|
||||
public ZenModeBlurbPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public ZenModeBlurbPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
if (holder.findViewById(android.R.id.title) instanceof TextView textView) {
|
||||
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX,
|
||||
getContext().getResources().getDimensionPixelSize(
|
||||
R.dimen.zen_mode_blurb_text_size));
|
||||
|
||||
textView.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
|
||||
|
||||
ViewGroup.LayoutParams layoutParams = textView.getLayoutParams();
|
||||
if (layoutParams.width != MATCH_PARENT) {
|
||||
layoutParams.width = MATCH_PARENT;
|
||||
textView.setLayoutParams(layoutParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package com.android.settings.notification.modes;
|
||||
|
||||
import static android.app.AutomaticZenRule.TYPE_BEDTIME;
|
||||
import static android.app.AutomaticZenRule.TYPE_DRIVING;
|
||||
import static android.app.AutomaticZenRule.TYPE_IMMERSIVE;
|
||||
import static android.app.AutomaticZenRule.TYPE_MANAGED;
|
||||
import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR;
|
||||
import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME;
|
||||
import static android.app.AutomaticZenRule.TYPE_THEATER;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settingslib.notification.modes.ZenMode;
|
||||
|
||||
class ZenModeBlurbPreferenceController extends AbstractZenModePreferenceController {
|
||||
|
||||
ZenModeBlurbPreferenceController(@NonNull Context context, @NonNull String key) {
|
||||
super(context, key);
|
||||
}
|
||||
|
||||
@Override
|
||||
void updateState(Preference preference, @NonNull ZenMode zenMode) {
|
||||
preference.setTitle(getModeBlurb(zenMode));
|
||||
}
|
||||
|
||||
@StringRes
|
||||
@SuppressLint("SwitchIntDef")
|
||||
private static int getModeBlurb(ZenMode mode) {
|
||||
if (mode.isSystemOwned()) {
|
||||
return switch (mode.getType()) {
|
||||
case TYPE_SCHEDULE_TIME -> R.string.zen_mode_blurb_schedule_time;
|
||||
case TYPE_SCHEDULE_CALENDAR -> R.string.zen_mode_blurb_schedule_calendar;
|
||||
default -> R.string.zen_mode_blurb_generic; // Custom Manual
|
||||
};
|
||||
} else {
|
||||
return switch (mode.getType()) {
|
||||
case TYPE_BEDTIME -> R.string.zen_mode_blurb_bedtime;
|
||||
case TYPE_DRIVING -> R.string.zen_mode_blurb_driving;
|
||||
case TYPE_IMMERSIVE -> R.string.zen_mode_blurb_immersive;
|
||||
case TYPE_THEATER -> R.string.zen_mode_blurb_theater;
|
||||
case TYPE_MANAGED -> R.string.zen_mode_blurb_managed;
|
||||
default -> R.string.zen_mode_blurb_generic; // Including OTHER, UNKNOWN.
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@ public class ZenModeFragment extends ZenModeFragmentBase {
|
||||
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
|
||||
List<AbstractPreferenceController> prefControllers = new ArrayList<>();
|
||||
prefControllers.add(new ZenModeHeaderController(context, "header", this));
|
||||
prefControllers.add(new ZenModeBlurbPreferenceController(context, "mode_blurb"));
|
||||
prefControllers.add(
|
||||
new ZenModeButtonPreferenceController(context, "activate", this, mBackend));
|
||||
prefControllers.add(new ZenModePreferenceCategoryController(context, "modes_filters"));
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
package com.android.settings.notification.modes;
|
||||
|
||||
import android.app.Flags;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -23,48 +22,22 @@ import androidx.preference.Preference;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.dashboard.DashboardFragment;
|
||||
import com.android.settings.widget.EntityHeaderController;
|
||||
import com.android.settingslib.notification.modes.ZenIconLoader;
|
||||
import com.android.settingslib.notification.modes.ZenMode;
|
||||
import com.android.settingslib.widget.LayoutPreference;
|
||||
|
||||
class ZenModeHeaderController extends AbstractZenModePreferenceController {
|
||||
|
||||
private final DashboardFragment mFragment;
|
||||
private EntityHeaderController mHeaderController;
|
||||
class ZenModeHeaderController extends AbstractZenModeHeaderController {
|
||||
|
||||
ZenModeHeaderController(
|
||||
@NonNull Context context,
|
||||
@NonNull String key,
|
||||
@NonNull DashboardFragment fragment) {
|
||||
super(context, key);
|
||||
mFragment = fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return Flags.modesApi();
|
||||
super(context, key, fragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateState(Preference preference, @NonNull ZenMode zenMode) {
|
||||
if (mFragment == null) {
|
||||
return;
|
||||
}
|
||||
preference.setSelectable(false);
|
||||
|
||||
if (mHeaderController == null) {
|
||||
final LayoutPreference pref = (LayoutPreference) preference;
|
||||
mHeaderController = EntityHeaderController.newInstance(
|
||||
mFragment.getActivity(),
|
||||
mFragment,
|
||||
pref.findViewById(R.id.entity_header));
|
||||
}
|
||||
|
||||
FutureUtil.whenDone(
|
||||
zenMode.getIcon(mContext, ZenIconLoader.getInstance()),
|
||||
icon -> mHeaderController.setIcon(IconUtil.applyNormalTint(mContext, icon))
|
||||
.done(/* rebindActions= */ false),
|
||||
mContext.getMainExecutor());
|
||||
updateIcon(preference, zenMode,
|
||||
mContext.getResources().getDimensionPixelSize(R.dimen.zen_mode_header_size),
|
||||
icon -> IconUtil.makeModeHeader(mContext, icon),
|
||||
iconView -> iconView.setSelected(zenMode.isActive()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,55 +17,28 @@
|
||||
package com.android.settings.notification.modes;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.dashboard.DashboardFragment;
|
||||
import com.android.settings.widget.EntityHeaderController;
|
||||
import com.android.settingslib.notification.modes.ZenIconLoader;
|
||||
import com.android.settingslib.notification.modes.ZenMode;
|
||||
import com.android.settingslib.widget.LayoutPreference;
|
||||
|
||||
/** Controller used for displaying the currently-chosen icon at the top of the icon picker. */
|
||||
class ZenModeIconPickerIconPreferenceController extends AbstractZenModePreferenceController {
|
||||
|
||||
private final DashboardFragment mFragment;
|
||||
private EntityHeaderController mHeaderController;
|
||||
class ZenModeIconPickerIconPreferenceController extends AbstractZenModeHeaderController {
|
||||
|
||||
ZenModeIconPickerIconPreferenceController(@NonNull Context context, @NonNull String key,
|
||||
@NonNull DashboardFragment fragment) {
|
||||
super(context, key);
|
||||
mFragment = fragment;
|
||||
super(context, key, fragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
void updateState(Preference preference, @NonNull ZenMode zenMode) {
|
||||
preference.setSelectable(false);
|
||||
|
||||
if (mHeaderController == null) {
|
||||
final LayoutPreference pref = (LayoutPreference) preference;
|
||||
mHeaderController = EntityHeaderController.newInstance(
|
||||
mFragment.getActivity(),
|
||||
mFragment,
|
||||
pref.findViewById(R.id.entity_header));
|
||||
|
||||
ImageView iconView = pref.findViewById(R.id.entity_header_icon);
|
||||
ViewGroup.LayoutParams layoutParams = iconView.getLayoutParams();
|
||||
int imageSizePx = iconView.getContext().getResources().getDimensionPixelSize(
|
||||
R.dimen.zen_mode_icon_list_header_circle_diameter);
|
||||
layoutParams.width = imageSizePx;
|
||||
layoutParams.height = imageSizePx;
|
||||
iconView.setLayoutParams(layoutParams);
|
||||
}
|
||||
|
||||
FutureUtil.whenDone(
|
||||
zenMode.getIcon(mContext, ZenIconLoader.getInstance()),
|
||||
icon -> mHeaderController.setIcon(IconUtil.makeIconPickerHeader(mContext, icon))
|
||||
.done(/* rebindActions= */ false),
|
||||
mContext.getMainExecutor());
|
||||
updateIcon(preference, zenMode,
|
||||
mContext.getResources().getDimensionPixelSize(
|
||||
R.dimen.zen_mode_icon_list_header_circle_diameter),
|
||||
icon -> IconUtil.makeIconPickerHeader(mContext, icon),
|
||||
null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,9 +46,10 @@ import com.android.settings.notification.modes.ZenHelperBackend.Contact;
|
||||
import com.android.settingslib.notification.ConversationIconFactory;
|
||||
import com.android.settingslib.notification.modes.ZenMode;
|
||||
|
||||
import com.google.common.base.Equivalence;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import java.util.function.Function;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Preference with a link and summary about what calls and messages can break through the mode,
|
||||
@@ -94,29 +95,60 @@ class ZenModePeopleLinkPreferenceController extends AbstractZenModePreferenceCon
|
||||
|
||||
preference.setEnabled(zenMode.isEnabled());
|
||||
preference.setSummary(mSummaryHelper.getPeopleSummary(zenMode.getPolicy()));
|
||||
((CircularIconsPreference) preference).displayIcons(getPeopleIcons(zenMode.getPolicy()));
|
||||
((CircularIconsPreference) preference).displayIcons(getPeopleIcons(zenMode.getPolicy()),
|
||||
PEOPLE_ITEM_EQUIVALENCE);
|
||||
}
|
||||
|
||||
// Represents "Either<Contact, ConversationChannelWrapper>".
|
||||
record PeopleItem(@Nullable Contact contact,
|
||||
@Nullable ConversationChannelWrapper conversation) {
|
||||
// Represents "Either<All, Contact, ConversationChannelWrapper>".
|
||||
private record PeopleItem(boolean all,
|
||||
@Nullable Contact contact,
|
||||
@Nullable ConversationChannelWrapper conversation) {
|
||||
|
||||
private static final PeopleItem ALL = new PeopleItem(true, null, null);
|
||||
|
||||
PeopleItem(@NonNull Contact contact) {
|
||||
this(contact, null);
|
||||
this(false, contact, null);
|
||||
}
|
||||
|
||||
PeopleItem(@NonNull ConversationChannelWrapper conversation) {
|
||||
this(null, conversation);
|
||||
this(false, null, conversation);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private CircularIconSet<?> getPeopleIcons(ZenPolicy policy) {
|
||||
private static final Equivalence<PeopleItem> PEOPLE_ITEM_EQUIVALENCE = new Equivalence<>() {
|
||||
@Override
|
||||
protected boolean doEquivalent(@NonNull PeopleItem a, @NonNull PeopleItem b) {
|
||||
if (a.all && b.all) {
|
||||
return true;
|
||||
} else if (a.contact != null && b.contact != null) {
|
||||
return a.contact.equals(b.contact);
|
||||
} else if (a.conversation != null && b.conversation != null) {
|
||||
ConversationChannelWrapper c1 = a.conversation;
|
||||
ConversationChannelWrapper c2 = b.conversation;
|
||||
// Skip comparing ShortcutInfo which doesn't implement equals(). We assume same
|
||||
// conversation channel means same icon (which is not 100% correct but unlikely to
|
||||
// change while on this screen).
|
||||
return Objects.equals(c1.getNotificationChannel(), c2.getNotificationChannel())
|
||||
&& Objects.equals(c1.getGroupLabel(), c2.getGroupLabel())
|
||||
&& Objects.equals(c1.getParentChannelLabel(), c2.getParentChannelLabel())
|
||||
&& Objects.equals(c1.getPkg(), c2.getPkg())
|
||||
&& Objects.equals(c1.getUid(), c2.getUid());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int doHash(@NonNull PeopleItem item) {
|
||||
return Objects.hash(item.all, item.contact, item.conversation);
|
||||
}
|
||||
};
|
||||
|
||||
private CircularIconSet<PeopleItem> getPeopleIcons(ZenPolicy policy) {
|
||||
if (getCallersOrMessagesAllowed(policy) == PEOPLE_TYPE_ANYONE) {
|
||||
return new CircularIconSet<>(
|
||||
ImmutableList.of(IconUtil.makeCircularIconPreferenceItem(mContext,
|
||||
R.drawable.ic_zen_mode_people_all)),
|
||||
Function.identity());
|
||||
ImmutableList.of(PeopleItem.ALL),
|
||||
this::loadPeopleIcon);
|
||||
}
|
||||
|
||||
ImmutableList.Builder<PeopleItem> peopleItems = ImmutableList.builder();
|
||||
@@ -181,7 +213,10 @@ class ZenModePeopleLinkPreferenceController extends AbstractZenModePreferenceCon
|
||||
|
||||
@WorkerThread
|
||||
private Drawable loadPeopleIcon(PeopleItem peopleItem) {
|
||||
if (peopleItem.contact != null) {
|
||||
if (peopleItem.all) {
|
||||
return IconUtil.makeCircularIconPreferenceItem(mContext,
|
||||
R.drawable.ic_zen_mode_people_all);
|
||||
} else if (peopleItem.contact != null) {
|
||||
return mHelperBackend.getContactPhoto(peopleItem.contact);
|
||||
} else if (peopleItem.conversation != null) {
|
||||
return mConversationIconFactory.getConversationDrawable(
|
||||
@@ -190,7 +225,7 @@ class ZenModePeopleLinkPreferenceController extends AbstractZenModePreferenceCon
|
||||
peopleItem.conversation.getUid(),
|
||||
/* important= */ true);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Neither contact nor conversation!");
|
||||
throw new IllegalArgumentException("Neither all nor contact nor conversation!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user