Merge "Add icon picker for zen modes" into main

This commit is contained in:
Matías Hernández
2024-06-10 22:38:36 +00:00
committed by Android (Google) Code Review
18 changed files with 665 additions and 11 deletions

View File

@@ -0,0 +1,69 @@
/*
* 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 com.google.common.base.Preconditions.checkNotNull;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import com.android.settings.R;
import com.android.settingslib.Utils;
class IconUtil {
static Drawable applyTint(@NonNull Context context, @NonNull Drawable icon) {
icon = icon.mutate();
icon.setTintList(
Utils.getColorAttr(context, android.R.attr.colorControlNormal));
return icon;
}
/**
* Returns a variant of the supplied {@code icon} to be used in the icon picker. The inner icon
* is 36x36dp and it's contained into a circle of diameter 54dp.
*/
static Drawable makeIconCircle(@NonNull Context context, @NonNull Drawable icon) {
ShapeDrawable background = new ShapeDrawable(new OvalShape());
background.getPaint().setColor(Utils.getColorAttrDefaultColor(context,
com.android.internal.R.attr.materialColorSecondaryContainer));
icon.setTint(Utils.getColorAttrDefaultColor(context,
com.android.internal.R.attr.materialColorOnSecondaryContainer));
LayerDrawable layerDrawable = new LayerDrawable(new Drawable[] { background, icon });
int circleDiameter = context.getResources().getDimensionPixelSize(
R.dimen.zen_mode_icon_list_circle_diameter);
int iconSize = context.getResources().getDimensionPixelSize(
R.dimen.zen_mode_icon_list_icon_size);
int iconPadding = (circleDiameter - iconSize) / 2;
layerDrawable.setBounds(0, 0, circleDiameter, circleDiameter);
layerDrawable.setLayerInset(1, iconPadding, iconPadding, iconPadding, iconPadding);
return layerDrawable;
}
static Drawable makeIconCircle(@NonNull Context context, @DrawableRes int iconResId) {
return makeIconCircle(context, checkNotNull(context.getDrawable(iconResId)));
}
}

View File

@@ -204,6 +204,14 @@ class ZenMode {
: new ZenDeviceEffects.Builder().build();
}
public boolean canEditName() {
return !isManualDnd();
}
public boolean canEditIcon() {
return !isManualDnd();
}
public boolean canBeDeleted() {
return !mIsManualDnd;
}

View File

@@ -0,0 +1,64 @@
/*
* 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 com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settings.core.SubSettingLauncher;
import com.android.settingslib.widget.ActionButtonsPreference;
class ZenModeActionsPreferenceController extends AbstractZenModePreferenceController {
private ActionButtonsPreference mPreference;
ZenModeActionsPreferenceController(@NonNull Context context, @NonNull String key,
@Nullable ZenModesBackend backend) {
super(context, key, backend);
}
@Override
void updateState(Preference preference, @NonNull ZenMode zenMode) {
ActionButtonsPreference buttonsPreference = (ActionButtonsPreference) preference;
// TODO: b/346278854 - Add rename action (with setButton1Enabled(zenMode.canEditName())
buttonsPreference.setButton1Text(R.string.zen_mode_action_change_name);
buttonsPreference.setButton1Icon(R.drawable.ic_mode_edit);
buttonsPreference.setButton1Enabled(false);
buttonsPreference.setButton2Text(R.string.zen_mode_action_change_icon);
buttonsPreference.setButton2Icon(R.drawable.ic_zen_mode_action_change_icon);
buttonsPreference.setButton2Enabled(zenMode.canEditIcon());
buttonsPreference.setButton2OnClickListener(v -> {
Bundle bundle = new Bundle();
bundle.putString(MODE_ID, zenMode.getId());
new SubSettingLauncher(mContext)
.setDestination(ZenModeIconPickerFragment.class.getName())
// TODO: b/332937635 - Update metrics category
.setSourceMetricsCategory(0)
.setArguments(bundle)
.launch();
});
}
}

View File

@@ -38,6 +38,7 @@ public class ZenModeFragment extends ZenModeFragmentBase {
List<AbstractPreferenceController> prefControllers = new ArrayList<>();
prefControllers.add(new ZenModeHeaderController(context, "header", this, mBackend));
prefControllers.add(new ZenModeButtonPreferenceController(context, "activate", mBackend));
prefControllers.add(new ZenModeActionsPreferenceController(context, "actions", mBackend));
prefControllers.add(new ZenModePeopleLinkPreferenceController(
context, "zen_mode_people", mBackend));
prefControllers.add(new ZenModeAppsLinkPreferenceController(

View File

@@ -51,12 +51,12 @@ abstract class ZenModeFragmentBase extends ZenModesFragmentBase {
if (bundle != null && bundle.containsKey(MODE_ID)) {
String id = bundle.getString(MODE_ID);
if (!reloadMode(id)) {
Log.d(TAG, "Mode id " + id + " not found");
Log.e(TAG, "Mode id " + id + " not found");
toastAndFinish();
return;
}
} else {
Log.d(TAG, "Mode id required to set mode config settings");
Log.e(TAG, "Mode id required to set mode config settings");
toastAndFinish();
return;
}

View File

@@ -63,9 +63,8 @@ class ZenModeHeaderController extends AbstractZenModePreferenceController {
FutureUtil.whenDone(
zenMode.getIcon(mContext, IconLoader.getInstance()),
icon -> mHeaderController.setIcon(icon)
.setLabel(zenMode.getRule().getName())
.done(false /* rebindActions */),
icon -> mHeaderController.setIcon(IconUtil.applyTint(mContext, icon))
.done(/* rebindActions= */ false),
mContext.getMainExecutor());
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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.settings.SettingsEnums;
import android.content.Context;
import com.android.settings.R;
import com.android.settingslib.core.AbstractPreferenceController;
import com.google.common.collect.ImmutableList;
import java.util.List;
public class ZenModeIconPickerFragment extends ZenModeFragmentBase {
@Override
protected int getPreferenceScreenResId() {
return R.xml.modes_icon_picker;
}
@Override
public int getMetricsCategory() {
// TODO: b/332937635 - make this the correct metrics category
return SettingsEnums.NOTIFICATION_ZEN_MODE_AUTOMATION;
}
@Override
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
return ImmutableList.of(
new ZenModeIconPickerIconPreferenceController(context, "current_icon", this,
mBackend),
new ZenModeIconPickerListPreferenceController(context, "icon_list", this,
mBackend));
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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.content.Context;
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.widget.LayoutPreference;
class ZenModeIconPickerIconPreferenceController extends AbstractZenModePreferenceController {
private final DashboardFragment mFragment;
private EntityHeaderController mHeaderController;
ZenModeIconPickerIconPreferenceController(@NonNull Context context, @NonNull String key,
@NonNull DashboardFragment fragment, @Nullable ZenModesBackend backend) {
super(context, key, backend);
mFragment = 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));
}
FutureUtil.whenDone(
zenMode.getIcon(mContext, IconLoader.getInstance()),
icon -> mHeaderController.setIcon(IconUtil.applyTint(mContext, icon))
.done(/* rebindActions= */ false),
mContext.getMainExecutor());
}
}

View File

@@ -0,0 +1,170 @@
/*
* 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.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.android.settings.R;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settingslib.widget.LayoutPreference;
import com.google.common.collect.ImmutableList;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
class ZenModeIconPickerListPreferenceController extends AbstractZenModePreferenceController {
private final DashboardFragment mFragment;
private IconAdapter mAdapter;
ZenModeIconPickerListPreferenceController(@NonNull Context context, @NonNull String key,
@NonNull DashboardFragment fragment, @Nullable ZenModesBackend backend) {
super(context, key, backend);
mFragment = fragment;
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
LayoutPreference pref = screen.findPreference(getPreferenceKey());
if (pref == null) {
return;
}
if (mAdapter == null) {
// TODO: b/333901673 - This is just an example; replace with correct list.
List<IconInfo> exampleIcons =
Arrays.stream(android.R.drawable.class.getFields())
.filter(
f -> Modifier.isStatic(f.getModifiers())
&& f.getName().startsWith("ic_"))
.sorted(Comparator.comparing(Field::getName))
.limit(20)
.map(f -> {
try {
return new IconInfo(f.getInt(null), f.getName());
} catch (IllegalAccessException e) {
return null;
}
})
.filter(Objects::nonNull)
.toList();
mAdapter = new IconAdapter(exampleIcons);
}
RecyclerView recyclerView = pref.findViewById(R.id.icon_list);
recyclerView.setLayoutManager(new AutoFitGridLayoutManager(mContext));
recyclerView.setAdapter(mAdapter);
recyclerView.setHasFixedSize(true);
}
@VisibleForTesting
void onIconSelected(@DrawableRes int resId) {
saveMode(mode -> {
mode.getRule().setIconResId(resId);
return mode;
});
mFragment.finish();
}
@Override
void updateState(Preference preference, @NonNull ZenMode zenMode) {
// Nothing to do, the current icon is shown in a different preference.
}
private record IconInfo(@DrawableRes int resId, String description) { }
private class IconHolder extends RecyclerView.ViewHolder {
private final ImageView mImageView;
IconHolder(@NonNull View itemView) {
super(itemView);
mImageView = itemView.findViewById(R.id.icon_image_view);
}
void bindIcon(IconInfo icon) {
mImageView.setImageDrawable(
IconUtil.makeIconCircle(itemView.getContext(), icon.resId()));
itemView.setContentDescription(icon.description());
itemView.setOnClickListener(v -> onIconSelected(icon.resId()));
}
}
private class IconAdapter extends RecyclerView.Adapter<IconHolder> {
private final ImmutableList<IconInfo> mIconResources;
private IconAdapter(List<IconInfo> iconOptions) {
mIconResources = ImmutableList.copyOf(iconOptions);
}
@NonNull
@Override
public IconHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext()).inflate(
R.layout.modes_icon_list_item, parent, false);
return new IconHolder(itemView);
}
@Override
public void onBindViewHolder(@NonNull IconHolder holder, int position) {
holder.bindIcon(mIconResources.get(position));
}
@Override
public int getItemCount() {
return mIconResources.size();
}
}
private static class AutoFitGridLayoutManager extends GridLayoutManager {
private final float mColumnWidth;
AutoFitGridLayoutManager(Context context) {
super(context, /* spanCount= */ 1);
this.mColumnWidth = context
.getResources()
.getDimensionPixelSize(R.dimen.zen_mode_icon_list_item_size);
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
final int totalSpace = getWidth() - getPaddingRight() - getPaddingLeft();
final int spanCount = Math.max(1, (int) (totalSpace / mColumnWidth));
setSpanCount(spanCount);
super.onLayoutChildren(recycler, state);
}
}
}

View File

@@ -23,7 +23,6 @@ import android.os.Bundle;
import com.android.settings.core.SubSettingLauncher;
import com.android.settingslib.RestrictedPreference;
import com.android.settingslib.Utils;
/**
* Preference representing a single mode item on the modes aggregator page. Clicking on this
@@ -59,11 +58,7 @@ class ZenModeListPreference extends RestrictedPreference {
FutureUtil.whenDone(
mZenMode.getIcon(mContext, IconLoader.getInstance()),
icon -> {
icon.setTintList(
Utils.getColorAttr(mContext, android.R.attr.colorControlNormal));
setIcon(icon);
},
icon -> setIcon(IconUtil.applyTint(mContext, icon)),
mContext.getMainExecutor());
}
}