Merge "Show icons for apps bypassing DND" into main
This commit is contained in:
committed by
Android (Google) Code Review
commit
bc0b3342bd
84
res/layout/preference_circular_icons.xml
Normal file
84
res/layout/preference_circular_icons.xml
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<?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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Based off preference_two_target.xml, with the added LinearLayout for the icons. -->
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minHeight="?android:attr/listPreferredItemHeightSmall"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||||
|
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||||
|
android:clipToPadding="false">
|
||||||
|
|
||||||
|
<include layout="@layout/settingslib_icon_frame"/>
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:paddingBottom="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@android:id/title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:hyphenationFrequency="normalFast"
|
||||||
|
android:lineBreakWordStyle="phrase"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||||
|
android:ellipsize="marquee"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@android:id/summary"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@android:id/title"
|
||||||
|
android:layout_alignStart="@android:id/title"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:hyphenationFrequency="normalFast"
|
||||||
|
android:lineBreakWordStyle="phrase"
|
||||||
|
android:maxLines="10"/>
|
||||||
|
|
||||||
|
<!-- Circular icons (32dp) will be ImageViews under this LinearLayout -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/circles_container"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_below="@android:id/summary"
|
||||||
|
android:layout_alignStart="@android:id/title" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<include layout="@layout/preference_two_target_divider" />
|
||||||
|
|
||||||
|
<!-- Preference should place its actual preference widget here. -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@android:id/widget_frame"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:minWidth="@dimen/two_target_min_width"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
24
res/layout/preference_circular_icons_item.xml
Normal file
24
res/layout/preference_circular_icons_item.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="@dimen/zen_mode_circular_icon_size"
|
||||||
|
android:layout_height="@dimen/zen_mode_circular_icon_size"
|
||||||
|
android:layout_marginTop="@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" />
|
@@ -508,4 +508,8 @@
|
|||||||
<dimen name="zen_mode_icon_list_item_size">96dp</dimen>
|
<dimen name="zen_mode_icon_list_item_size">96dp</dimen>
|
||||||
<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). -->
|
||||||
|
<dimen name="zen_mode_circular_icon_size">32dp</dimen>
|
||||||
|
<dimen name="zen_mode_circular_icon_margin_between">4dp</dimen>
|
||||||
|
<dimen name="zen_mode_circular_icon_margin_vertical">8dp</dimen>
|
||||||
</resources>
|
</resources>
|
||||||
|
@@ -36,17 +36,17 @@
|
|||||||
android:key="allow_filtering"
|
android:key="allow_filtering"
|
||||||
android:title="@string/mode_notification_filter_title"/>
|
android:title="@string/mode_notification_filter_title"/>
|
||||||
|
|
||||||
<Preference
|
<com.android.settings.notification.modes.CircularIconsPreference
|
||||||
android:key="zen_mode_people"
|
android:key="zen_mode_people"
|
||||||
android:title="@string/zen_category_people"/>
|
android:title="@string/zen_category_people" />
|
||||||
|
|
||||||
<Preference
|
<com.android.settings.notification.modes.CircularIconsPreference
|
||||||
android:key="zen_mode_apps"
|
android:key="zen_mode_apps"
|
||||||
android:title="@string/zen_category_apps"/>
|
android:title="@string/zen_category_apps" />
|
||||||
|
|
||||||
<Preference
|
<com.android.settings.notification.modes.CircularIconsPreference
|
||||||
android:key="zen_other_settings"
|
android:key="zen_other_settings"
|
||||||
android:title="@string/zen_category_exceptions" />
|
android:title="@string/zen_category_exceptions" />
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<!-- automatic trigger section; preference changes programmatically depending on type -->
|
<!-- automatic trigger section; preference changes programmatically depending on type -->
|
||||||
|
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* 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.graphics.Color;
|
||||||
|
import android.graphics.drawable.ColorDrawable;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import com.google.common.util.concurrent.ListeningExecutorService;
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of icons to be displayed in a {@link CircularIconsPreference}
|
||||||
|
*
|
||||||
|
* @param <T> The type of the items in the set. Can be an arbitrary type, the only requirement
|
||||||
|
* being that the {@code drawableLoader} supplied to the constructor is able to produce
|
||||||
|
* a {@link Drawable} from it (for example a resource id, a Content Uri, etc).
|
||||||
|
*/
|
||||||
|
class CircularIconSet<T> {
|
||||||
|
|
||||||
|
@VisibleForTesting // Can be set by tests, before creating instances.
|
||||||
|
static ExecutorService sExecutorService = Executors.newCachedThreadPool();
|
||||||
|
|
||||||
|
static final CircularIconSet<?> EMPTY = new CircularIconSet<>(ImmutableList.of(),
|
||||||
|
unused -> new ColorDrawable(Color.BLACK));
|
||||||
|
|
||||||
|
private final ImmutableList<T> mItems;
|
||||||
|
private final Function<T, Drawable> mDrawableLoader;
|
||||||
|
private final ListeningExecutorService mBackgroundExecutor;
|
||||||
|
|
||||||
|
private final ConcurrentHashMap<T, Drawable> mCachedIcons;
|
||||||
|
|
||||||
|
CircularIconSet(List<T> items, Function<T, Drawable> drawableLoader) {
|
||||||
|
mItems = ImmutableList.copyOf(items);
|
||||||
|
mDrawableLoader = drawableLoader;
|
||||||
|
mBackgroundExecutor = MoreExecutors.listeningDecorator(sExecutorService);
|
||||||
|
mCachedIcons = new ConcurrentHashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
int size() {
|
||||||
|
return mItems.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads all icons from the set, using the supplied {@code drawableLoader}, in a background
|
||||||
|
* thread.
|
||||||
|
*/
|
||||||
|
List<ListenableFuture<Drawable>> getIcons() {
|
||||||
|
return getIcons(Integer.MAX_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads up to {@code maxSize} icons from the set, using the supplied {@code drawableLoader}, in
|
||||||
|
* a background thread.
|
||||||
|
*/
|
||||||
|
List<ListenableFuture<Drawable>> getIcons(int maxNumber) {
|
||||||
|
return mItems.stream().limit(maxNumber)
|
||||||
|
.map(this::loadIcon)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListenableFuture<Drawable> loadIcon(T item) {
|
||||||
|
return mBackgroundExecutor.submit(() -> {
|
||||||
|
if (mCachedIcons.containsKey(item)) {
|
||||||
|
return mCachedIcons.get(item);
|
||||||
|
}
|
||||||
|
Drawable drawable = mDrawableLoader.apply(item);
|
||||||
|
if (drawable != null) {
|
||||||
|
mCachedIcons.put(item, drawable);
|
||||||
|
}
|
||||||
|
return drawable;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,208 @@
|
|||||||
|
/*
|
||||||
|
* 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 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.util.AttributeSet;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewTreeObserver;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
import androidx.preference.PreferenceViewHolder;
|
||||||
|
|
||||||
|
import com.android.settings.R;
|
||||||
|
import com.android.settingslib.RestrictedPreference;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.util.concurrent.Futures;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
public class CircularIconsPreference extends RestrictedPreference {
|
||||||
|
|
||||||
|
private Executor mUiExecutor;
|
||||||
|
@Nullable private LinearLayout mIconContainer;
|
||||||
|
|
||||||
|
@Nullable private CircularIconSet<?> mPendingIconSet;
|
||||||
|
@Nullable private ListenableFuture<?> mPendingLoadIconsFuture;
|
||||||
|
|
||||||
|
public CircularIconsPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
init(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||||
|
public CircularIconsPreference(Context context, Executor uiExecutor) {
|
||||||
|
this(context);
|
||||||
|
mUiExecutor = uiExecutor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CircularIconsPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
init(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CircularIconsPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
init(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CircularIconsPreference(Context context, AttributeSet attrs, int defStyleAttr,
|
||||||
|
int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
init(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init(Context context) {
|
||||||
|
mUiExecutor = context.getMainExecutor();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void displayIconsIfPending() {
|
||||||
|
CircularIconSet<?> pendingIconSet = mPendingIconSet;
|
||||||
|
if (pendingIconSet != null) {
|
||||||
|
mPendingIconSet = null;
|
||||||
|
displayIcons(pendingIconSet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void displayIcons(CircularIconSet<?> iconSet) {
|
||||||
|
if (mIconContainer == null) {
|
||||||
|
// Too soon, wait for bind.
|
||||||
|
mPendingIconSet = 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.
|
||||||
|
mPendingIconSet = iconSet;
|
||||||
|
ViewTreeObserver vto = mIconContainer.getViewTreeObserver();
|
||||||
|
vto.addOnGlobalLayoutListener(() ->
|
||||||
|
new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||||
|
@Override
|
||||||
|
public void onGlobalLayout() {
|
||||||
|
vto.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)
|
||||||
|
+ res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_margin_between);
|
||||||
|
int numIconsThatFit = availableSpace / iconHorizontalSpace;
|
||||||
|
|
||||||
|
List<ListenableFuture<Drawable>> iconFutures;
|
||||||
|
int extraItems = 0;
|
||||||
|
if (iconSet.size() > numIconsThatFit) {
|
||||||
|
// Reserve one space for the (+xx) circle.
|
||||||
|
int numIconsToShow = numIconsThatFit - 1;
|
||||||
|
if (numIconsToShow < 0) {
|
||||||
|
numIconsToShow = 0;
|
||||||
|
}
|
||||||
|
iconFutures = iconSet.getIcons(numIconsToShow);
|
||||||
|
extraItems = iconSet.size() - numIconsToShow;
|
||||||
|
} else {
|
||||||
|
// Fit exactly or with remaining space.
|
||||||
|
iconFutures = iconSet.getIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
displayIconsWhenReady(iconFutures, extraItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void displayIconsWhenReady(List<ListenableFuture<Drawable>> iconFutures,
|
||||||
|
int extraItems) {
|
||||||
|
checkState(mIconContainer != null);
|
||||||
|
if (mPendingLoadIconsFuture != null) {
|
||||||
|
mPendingLoadIconsFuture.cancel(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
int numCircles = iconFutures.size() + (extraItems > 0 ? 1 : 0);
|
||||||
|
if (mIconContainer.getChildCount() > numCircles) {
|
||||||
|
mIconContainer.removeViews(numCircles, mIconContainer.getChildCount() - numCircles);
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up placeholders and extra items indicator.
|
||||||
|
for (int i = 0; i < iconFutures.size(); 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));
|
||||||
|
}
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display icons when all are ready (more consistent than randomly loading).
|
||||||
|
mPendingLoadIconsFuture = Futures.allAsList(iconFutures);
|
||||||
|
FutureUtil.whenDone(
|
||||||
|
Futures.allAsList(iconFutures),
|
||||||
|
icons -> {
|
||||||
|
checkState(mIconContainer != null);
|
||||||
|
for (int i = 0; i < icons.size(); i++) {
|
||||||
|
((ImageView) mIconContainer.getChildAt(i)).setImageDrawable(icons.get(i));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mUiExecutor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||||
|
ImmutableList<ImageView> getIconViews() {
|
||||||
|
if (mIconContainer == null) {
|
||||||
|
return ImmutableList.of();
|
||||||
|
}
|
||||||
|
ImmutableList.Builder<ImageView> imageViews = new ImmutableList.Builder<>();
|
||||||
|
for (int i = 0; i < mIconContainer.getChildCount(); i++) {
|
||||||
|
imageViews.add((ImageView) mIconContainer.getChildAt(i));
|
||||||
|
}
|
||||||
|
return imageViews.build();
|
||||||
|
}
|
||||||
|
}
|
@@ -18,10 +18,13 @@ package com.android.settings.notification.modes;
|
|||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.google.common.util.concurrent.FutureCallback;
|
import com.google.common.util.concurrent.FutureCallback;
|
||||||
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.concurrent.CancellationException;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@@ -42,8 +45,10 @@ class FutureUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Throwable throwable) {
|
public void onFailure(@NonNull Throwable throwable) {
|
||||||
Log.e(TAG, String.format(errorLogMessage, errorLogMessageArgs), throwable);
|
if (!(throwable instanceof CancellationException)) {
|
||||||
|
Log.e(TAG, String.format(errorLogMessage, errorLogMessageArgs), throwable);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, executor);
|
}, executor);
|
||||||
}
|
}
|
||||||
|
@@ -24,6 +24,7 @@ import android.content.Context;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.UserHandle;
|
import android.os.UserHandle;
|
||||||
import android.os.UserManager;
|
import android.os.UserManager;
|
||||||
|
import android.service.notification.ZenPolicy;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -32,6 +33,7 @@ import androidx.fragment.app.Fragment;
|
|||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
|
|
||||||
import com.android.settings.R;
|
import com.android.settings.R;
|
||||||
|
import com.android.settings.Utils;
|
||||||
import com.android.settings.core.SubSettingLauncher;
|
import com.android.settings.core.SubSettingLauncher;
|
||||||
import com.android.settingslib.applications.ApplicationsState;
|
import com.android.settingslib.applications.ApplicationsState;
|
||||||
import com.android.settingslib.applications.ApplicationsState.AppEntry;
|
import com.android.settingslib.applications.ApplicationsState.AppEntry;
|
||||||
@@ -59,7 +61,7 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr
|
|||||||
private ApplicationsState.Session mAppSession;
|
private ApplicationsState.Session mAppSession;
|
||||||
private final ZenHelperBackend mHelperBackend;
|
private final ZenHelperBackend mHelperBackend;
|
||||||
private ZenMode mZenMode;
|
private ZenMode mZenMode;
|
||||||
private Preference mPreference;
|
private CircularIconsPreference mPreference;
|
||||||
private final Fragment mHost;
|
private final Fragment mHost;
|
||||||
|
|
||||||
ZenModeAppsLinkPreferenceController(Context context, String key, Fragment host,
|
ZenModeAppsLinkPreferenceController(Context context, String key, Fragment host,
|
||||||
@@ -97,14 +99,21 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr
|
|||||||
.setArguments(bundle)
|
.setArguments(bundle)
|
||||||
.toIntent());
|
.toIntent());
|
||||||
mZenMode = zenMode;
|
mZenMode = zenMode;
|
||||||
mPreference = preference;
|
mPreference = (CircularIconsPreference) preference;
|
||||||
if (TextUtils.isEmpty(mPreference.getSummary())) {
|
|
||||||
mPreference.setSummary(R.string.zen_mode_apps_calculating);
|
if (zenMode.getPolicy().getAllowedChannels() == ZenPolicy.CHANNEL_POLICY_NONE) {
|
||||||
|
mPreference.setSummary(R.string.zen_mode_apps_none_apps);
|
||||||
|
mPreference.displayIcons(CircularIconSet.EMPTY);
|
||||||
|
} else {
|
||||||
|
if (TextUtils.isEmpty(mPreference.getSummary())) {
|
||||||
|
mPreference.setSummary(R.string.zen_mode_apps_calculating);
|
||||||
|
}
|
||||||
|
if (mApplicationsState != null && mHost != null) {
|
||||||
|
mAppSession = mApplicationsState.newSession(mAppSessionCallbacks,
|
||||||
|
mHost.getLifecycle());
|
||||||
|
}
|
||||||
|
triggerUpdateAppsBypassingDnd();
|
||||||
}
|
}
|
||||||
if (mApplicationsState != null && mHost != null) {
|
|
||||||
mAppSession = mApplicationsState.newSession(mAppSessionCallbacks, mHost.getLifecycle());
|
|
||||||
}
|
|
||||||
triggerUpdateAppsBypassingDnd();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void triggerUpdateAppsBypassingDnd() {
|
private void triggerUpdateAppsBypassingDnd() {
|
||||||
@@ -126,6 +135,9 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr
|
|||||||
ImmutableList<AppEntry> apps = getAppsBypassingDndSortedByName(allApps);
|
ImmutableList<AppEntry> apps = getAppsBypassingDndSortedByName(allApps);
|
||||||
|
|
||||||
mPreference.setSummary(mSummaryHelper.getAppsSummary(mZenMode, apps));
|
mPreference.setSummary(mSummaryHelper.getAppsSummary(mZenMode, apps));
|
||||||
|
|
||||||
|
mPreference.displayIcons(new CircularIconSet<>(apps,
|
||||||
|
app -> Utils.getBadgedIcon(mContext, app.info)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
|
@@ -17,15 +17,12 @@
|
|||||||
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.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
|
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
|
|
||||||
import com.android.settings.core.SubSettingLauncher;
|
|
||||||
import com.android.settingslib.notification.modes.ZenMode;
|
import com.android.settingslib.notification.modes.ZenMode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,13 +45,13 @@ class ZenModeOtherLinkPreferenceController extends AbstractZenModePreferenceCont
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateState(Preference preference, @NonNull ZenMode zenMode) {
|
public void updateState(Preference preference, @NonNull ZenMode zenMode) {
|
||||||
Bundle bundle = new Bundle();
|
// TODO: b/332937635 - Update metrics category
|
||||||
bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, zenMode.getId());
|
preference.setIntent(
|
||||||
preference.setIntent(new SubSettingLauncher(mContext)
|
ZenSubSettingLauncher.forModeFragment(mContext, ZenModeOtherFragment.class,
|
||||||
.setDestination(ZenModeOtherFragment.class.getName())
|
zenMode.getId(), 0).toIntent());
|
||||||
.setSourceMetricsCategory(0)
|
|
||||||
.setArguments(bundle)
|
|
||||||
.toIntent());
|
|
||||||
preference.setSummary(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode));
|
preference.setSummary(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode));
|
||||||
|
// TODO: b/346551087 - Show media icons
|
||||||
|
((CircularIconsPreference) preference).displayIcons(CircularIconSet.EMPTY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -17,15 +17,12 @@
|
|||||||
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.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
|
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
|
|
||||||
import com.android.settings.core.SubSettingLauncher;
|
|
||||||
import com.android.settingslib.notification.modes.ZenMode;
|
import com.android.settingslib.notification.modes.ZenMode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,14 +45,13 @@ class ZenModePeopleLinkPreferenceController extends AbstractZenModePreferenceCon
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateState(Preference preference, @NonNull ZenMode zenMode) {
|
public void updateState(Preference preference, @NonNull ZenMode zenMode) {
|
||||||
Bundle bundle = new Bundle();
|
|
||||||
bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, zenMode.getId());
|
|
||||||
// TODO(b/332937635): Update metrics category
|
// TODO(b/332937635): Update metrics category
|
||||||
preference.setIntent(new SubSettingLauncher(mContext)
|
preference.setIntent(
|
||||||
.setDestination(ZenModePeopleFragment.class.getName())
|
ZenSubSettingLauncher.forModeFragment(mContext, ZenModePeopleFragment.class,
|
||||||
.setSourceMetricsCategory(0)
|
zenMode.getId(), 0).toIntent());
|
||||||
.setArguments(bundle)
|
|
||||||
.toIntent());
|
|
||||||
preference.setSummary(mSummaryHelper.getPeopleSummary(zenMode));
|
preference.setSummary(mSummaryHelper.getPeopleSummary(zenMode));
|
||||||
|
// TODO: b/346551087 - Show people icons
|
||||||
|
((CircularIconsPreference) preference).displayIcons(CircularIconSet.EMPTY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* 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.truth.Truth.assertThat;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.drawable.ColorDrawable;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.robolectric.RobolectricTestRunner;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner.class)
|
||||||
|
public class CircularIconSetTest {
|
||||||
|
|
||||||
|
@Mock private Function<Integer, Drawable> mDrawableLoader;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
MockitoAnnotations.initMocks(this);
|
||||||
|
CircularIconSet.sExecutorService = MoreExecutors.newDirectExecutorService();
|
||||||
|
when(mDrawableLoader.apply(anyInt())).thenReturn(new ColorDrawable(Color.BLACK));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getIcons_loadsAllIcons() {
|
||||||
|
CircularIconSet<Integer> set = new CircularIconSet<>(ImmutableList.of(1, 2, 3),
|
||||||
|
mDrawableLoader);
|
||||||
|
|
||||||
|
List<ListenableFuture<Drawable>> iconFutures = set.getIcons();
|
||||||
|
|
||||||
|
assertThat(iconFutures).hasSize(3);
|
||||||
|
verify(mDrawableLoader).apply(1);
|
||||||
|
verify(mDrawableLoader).apply(2);
|
||||||
|
verify(mDrawableLoader).apply(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getIcons_loadsRequestedIcons() {
|
||||||
|
CircularIconSet<Integer> set = new CircularIconSet<>(ImmutableList.of(1, 2, 3, 4, 5),
|
||||||
|
mDrawableLoader);
|
||||||
|
|
||||||
|
List<ListenableFuture<Drawable>> iconFutures = set.getIcons(2);
|
||||||
|
|
||||||
|
assertThat(iconFutures).hasSize(2);
|
||||||
|
verify(mDrawableLoader).apply(1);
|
||||||
|
verify(mDrawableLoader).apply(2);
|
||||||
|
verifyNoMoreInteractions(mDrawableLoader);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getIcons_cachesIcons() {
|
||||||
|
CircularIconSet<Integer> set = new CircularIconSet<>(ImmutableList.of(1, 2, 3, 4, 5),
|
||||||
|
mDrawableLoader);
|
||||||
|
|
||||||
|
List<ListenableFuture<Drawable>> iconFutures = set.getIcons(2);
|
||||||
|
assertThat(iconFutures).hasSize(2);
|
||||||
|
verify(mDrawableLoader).apply(1);
|
||||||
|
verify(mDrawableLoader).apply(2);
|
||||||
|
verifyNoMoreInteractions(mDrawableLoader);
|
||||||
|
|
||||||
|
List<ListenableFuture<Drawable>> iconFuturesAgain = set.getIcons(3);
|
||||||
|
assertThat(iconFuturesAgain).hasSize(3);
|
||||||
|
verify(mDrawableLoader).apply(3);
|
||||||
|
verifyNoMoreInteractions(mDrawableLoader);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,201 @@
|
|||||||
|
/*
|
||||||
|
* 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.View.MeasureSpec.makeMeasureSpec;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
|
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.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
import androidx.preference.PreferenceViewHolder;
|
||||||
|
|
||||||
|
import com.android.settings.R;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.util.concurrent.Futures;
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.robolectric.RobolectricTestRunner;
|
||||||
|
import org.robolectric.RuntimeEnvironment;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner.class)
|
||||||
|
public class CircularIconsPreferenceTest {
|
||||||
|
|
||||||
|
private static final int VIEW_WIDTH = 800;
|
||||||
|
|
||||||
|
private Context mContext;
|
||||||
|
private CircularIconsPreference mPreference;
|
||||||
|
private View mIconContainer;
|
||||||
|
|
||||||
|
private int mOneIconWidth;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() {
|
||||||
|
MockitoAnnotations.initMocks(this);
|
||||||
|
mContext = RuntimeEnvironment.application;
|
||||||
|
CircularIconSet.sExecutorService = MoreExecutors.newDirectExecutorService();
|
||||||
|
mPreference = new CircularIconsPreference(mContext, MoreExecutors.directExecutor());
|
||||||
|
// Tests should call bindAndMeasureViewHolder() so that icons can be added.
|
||||||
|
|
||||||
|
Resources res = mContext.getResources();
|
||||||
|
mOneIconWidth = res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_size)
|
||||||
|
+ res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_margin_between);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bindAndMeasureViewHolder(int viewWidth) {
|
||||||
|
View preferenceView = LayoutInflater.from(mContext).inflate(mPreference.getLayoutResource(),
|
||||||
|
null);
|
||||||
|
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);
|
||||||
|
mPreference.onBindViewHolder(holder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void displayIcons_loadsIcons() {
|
||||||
|
CircularIconSet<Integer> iconSet = new CircularIconSet<>(ImmutableList.of(1, 2),
|
||||||
|
ColorDrawable::new);
|
||||||
|
|
||||||
|
bindAndMeasureViewHolder(VIEW_WIDTH);
|
||||||
|
mPreference.displayIcons(iconSet);
|
||||||
|
|
||||||
|
assertThat(mPreference.getIconViews()).hasSize(2);
|
||||||
|
assertThat(mPreference.getIconViews().get(0).getDrawable())
|
||||||
|
.isInstanceOf(ColorDrawable.class);
|
||||||
|
assertThat(((ColorDrawable) mPreference.getIconViews().get(0).getDrawable()).getColor())
|
||||||
|
.isEqualTo(1);
|
||||||
|
assertThat(((ColorDrawable) mPreference.getIconViews().get(1).getDrawable()).getColor())
|
||||||
|
.isEqualTo(2);
|
||||||
|
assertThat(mIconContainer.getVisibility()).isEqualTo(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void displayIcons_noIcons_hidesRow() {
|
||||||
|
CircularIconSet<Integer> iconSet = new CircularIconSet<>(ImmutableList.of(),
|
||||||
|
ColorDrawable::new);
|
||||||
|
|
||||||
|
bindAndMeasureViewHolder(VIEW_WIDTH);
|
||||||
|
mPreference.displayIcons(iconSet);
|
||||||
|
|
||||||
|
assertThat(mIconContainer.getVisibility()).isEqualTo(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void displayIcons_exactlyMaxIcons_loadsAllIcons() throws Exception {
|
||||||
|
int width = 300;
|
||||||
|
int fittingIcons = width / mOneIconWidth;
|
||||||
|
CircularIconSet<Integer> iconSet = new CircularIconSet<>(
|
||||||
|
IntStream.range(0, fittingIcons).boxed().toList(),
|
||||||
|
ColorDrawable::new);
|
||||||
|
|
||||||
|
bindAndMeasureViewHolder(width);
|
||||||
|
mPreference.displayIcons(iconSet);
|
||||||
|
|
||||||
|
List<Drawable> displayedDrawables = mPreference.getIconViews().stream()
|
||||||
|
.map(ImageView::getDrawable).toList();
|
||||||
|
assertThat(displayedDrawables).hasSize(fittingIcons);
|
||||||
|
assertThat(displayedDrawables).containsExactlyElementsIn(
|
||||||
|
Futures.allAsList(iconSet.getIcons()).get()).inOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void displayIcons_tooManyIcons_loadsFirstNAndPlusIcon() throws Exception {
|
||||||
|
int width = 300;
|
||||||
|
int fittingIcons = width / mOneIconWidth;
|
||||||
|
CircularIconSet<Integer> iconSet = new CircularIconSet<>(
|
||||||
|
IntStream.range(0, fittingIcons + 5).boxed().toList(),
|
||||||
|
ColorDrawable::new);
|
||||||
|
|
||||||
|
bindAndMeasureViewHolder(width);
|
||||||
|
mPreference.displayIcons(iconSet);
|
||||||
|
|
||||||
|
List<Drawable> displayedDrawables = mPreference.getIconViews().stream()
|
||||||
|
.map(ImageView::getDrawable).toList();
|
||||||
|
assertThat(displayedDrawables).hasSize(fittingIcons);
|
||||||
|
// N-1 are actual icons, Nth icon is (+xx).
|
||||||
|
assertThat(displayedDrawables.stream().limit(fittingIcons - 1).toList())
|
||||||
|
.containsExactlyElementsIn(
|
||||||
|
Futures.allAsList(iconSet.getIcons(fittingIcons - 1)).get())
|
||||||
|
.inOrder();
|
||||||
|
// TODO: b/346551087 - Correctly verify the plus-6 icon, once we generate it properly.
|
||||||
|
assertThat(((ColorDrawable) displayedDrawables.get(
|
||||||
|
displayedDrawables.size() - 1)).getColor()).isEqualTo(Color.BLUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void displayIcons_teenyTinySpace_showsPlusIcon_noCrash() {
|
||||||
|
int width = 1;
|
||||||
|
CircularIconSet<Integer> iconSet = new CircularIconSet<>(ImmutableList.of(1, 2),
|
||||||
|
ColorDrawable::new);
|
||||||
|
|
||||||
|
bindAndMeasureViewHolder(width);
|
||||||
|
mPreference.displayIcons(iconSet);
|
||||||
|
|
||||||
|
assertThat(mPreference.getIconViews()).hasSize(1);
|
||||||
|
// TODO: b/346551087 - Correctly verify the plus-2 icon, once we generate it properly.
|
||||||
|
assertThat(((ColorDrawable) mPreference.getIconViews().get(0).getDrawable()).getColor())
|
||||||
|
.isEqualTo(Color.BLUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void displayIcons_beforeBind_loadsIconsOnBind() {
|
||||||
|
CircularIconSet<Integer> iconSet = new CircularIconSet<>(ImmutableList.of(1, 2, 3),
|
||||||
|
ColorDrawable::new);
|
||||||
|
|
||||||
|
mPreference.displayIcons(iconSet);
|
||||||
|
assertThat(mPreference.getIconViews()).isEmpty();
|
||||||
|
|
||||||
|
bindAndMeasureViewHolder(VIEW_WIDTH);
|
||||||
|
assertThat(mPreference.getIconViews()).hasSize(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void displayIcons_calledAgain_reloadsIcons() {
|
||||||
|
CircularIconSet<Integer> threeIcons = new CircularIconSet<>(ImmutableList.of(1, 2, 3),
|
||||||
|
ColorDrawable::new);
|
||||||
|
CircularIconSet<Integer> twoIcons = new CircularIconSet<>(ImmutableList.of(1, 2),
|
||||||
|
ColorDrawable::new);
|
||||||
|
CircularIconSet<Integer> fourIcons = new CircularIconSet<>(ImmutableList.of(1, 2, 3, 4),
|
||||||
|
ColorDrawable::new);
|
||||||
|
bindAndMeasureViewHolder(VIEW_WIDTH);
|
||||||
|
|
||||||
|
mPreference.displayIcons(threeIcons);
|
||||||
|
assertThat(mPreference.getIconViews()).hasSize(3);
|
||||||
|
mPreference.displayIcons(twoIcons);
|
||||||
|
assertThat(mPreference.getIconViews()).hasSize(2);
|
||||||
|
mPreference.displayIcons(fourIcons);
|
||||||
|
assertThat(mPreference.getIconViews()).hasSize(4);
|
||||||
|
}
|
||||||
|
}
|
@@ -23,10 +23,13 @@ import static com.google.common.truth.Truth.assertThat;
|
|||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
import static org.robolectric.Shadows.shadowOf;
|
import static org.robolectric.Shadows.shadowOf;
|
||||||
|
|
||||||
@@ -41,8 +44,11 @@ import android.os.UserManager;
|
|||||||
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 android.service.notification.ZenPolicy;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.preference.PreferenceViewHolder;
|
||||||
|
|
||||||
import com.android.settings.SettingsActivity;
|
import com.android.settings.SettingsActivity;
|
||||||
import com.android.settingslib.applications.ApplicationsState;
|
import com.android.settingslib.applications.ApplicationsState;
|
||||||
@@ -51,7 +57,9 @@ import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
|
|||||||
import com.android.settingslib.notification.modes.TestModeBuilder;
|
import com.android.settingslib.notification.modes.TestModeBuilder;
|
||||||
import com.android.settingslib.notification.modes.ZenMode;
|
import com.android.settingslib.notification.modes.ZenMode;
|
||||||
import com.android.settingslib.notification.modes.ZenModesBackend;
|
import com.android.settingslib.notification.modes.ZenModesBackend;
|
||||||
import com.android.settingslib.widget.SelectorWithWidgetPreference;
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
@@ -71,6 +79,7 @@ import java.util.Random;
|
|||||||
public final class ZenModeAppsLinkPreferenceControllerTest {
|
public final class ZenModeAppsLinkPreferenceControllerTest {
|
||||||
|
|
||||||
private ZenModeAppsLinkPreferenceController mController;
|
private ZenModeAppsLinkPreferenceController mController;
|
||||||
|
private CircularIconsPreference mPreference;
|
||||||
|
|
||||||
private Context mContext;
|
private Context mContext;
|
||||||
@Mock
|
@Mock
|
||||||
@@ -91,10 +100,21 @@ public final class ZenModeAppsLinkPreferenceControllerTest {
|
|||||||
public void setup() {
|
public void setup() {
|
||||||
MockitoAnnotations.initMocks(this);
|
MockitoAnnotations.initMocks(this);
|
||||||
mContext = RuntimeEnvironment.application;
|
mContext = RuntimeEnvironment.application;
|
||||||
|
CircularIconSet.sExecutorService = MoreExecutors.newDirectExecutorService();
|
||||||
|
mPreference = new CircularIconsPreference(mContext, MoreExecutors.directExecutor());
|
||||||
|
|
||||||
when(mApplicationsState.newSession(any(), any())).thenReturn(mSession);
|
when(mApplicationsState.newSession(any(), any())).thenReturn(mSession);
|
||||||
mController = new ZenModeAppsLinkPreferenceController(
|
mController = new ZenModeAppsLinkPreferenceController(
|
||||||
mContext, "controller_key", mock(Fragment.class), mApplicationsState,
|
mContext, "controller_key", mock(Fragment.class), mApplicationsState,
|
||||||
mZenModesBackend, mHelperBackend);
|
mZenModesBackend, mHelperBackend);
|
||||||
|
|
||||||
|
// Ensure the preference view is bound & measured (needed to add child ImageViews).
|
||||||
|
View preferenceView = LayoutInflater.from(mContext).inflate(mPreference.getLayoutResource(),
|
||||||
|
null);
|
||||||
|
preferenceView.measure(View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY),
|
||||||
|
View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY));
|
||||||
|
PreferenceViewHolder holder = PreferenceViewHolder.createInstanceForTests(preferenceView);
|
||||||
|
mPreference.onBindViewHolder(holder);
|
||||||
}
|
}
|
||||||
|
|
||||||
private AppEntry createAppEntry(String packageName, int userId) {
|
private AppEntry createAppEntry(String packageName, int userId) {
|
||||||
@@ -123,13 +143,11 @@ public final class ZenModeAppsLinkPreferenceControllerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testUpdateSetsIntent() {
|
public void testUpdateSetsIntent() {
|
||||||
// Creates the preference
|
|
||||||
SelectorWithWidgetPreference preference = new SelectorWithWidgetPreference(mContext);
|
|
||||||
// Create a zen mode that allows priority channels to breakthrough.
|
// Create a zen mode that allows priority channels to breakthrough.
|
||||||
ZenMode zenMode = createPriorityChannelsZenMode();
|
ZenMode zenMode = createPriorityChannelsZenMode();
|
||||||
|
|
||||||
mController.updateState(preference, zenMode);
|
mController.updateState(mPreference, zenMode);
|
||||||
Intent launcherIntent = preference.getIntent();
|
Intent launcherIntent = mPreference.getIntent();
|
||||||
|
|
||||||
assertThat(launcherIntent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT))
|
assertThat(launcherIntent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT))
|
||||||
.isEqualTo("com.android.settings.notification.modes.ZenModeAppsFragment");
|
.isEqualTo("com.android.settings.notification.modes.ZenModeAppsFragment");
|
||||||
@@ -193,9 +211,20 @@ public final class ZenModeAppsLinkPreferenceControllerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testUpdateTriggersRebuild() {
|
public void updateState_withPolicyAllowingNoChannels_doesNotLoadPriorityApps() {
|
||||||
// Creates the preference
|
ZenMode zenMode = new TestModeBuilder()
|
||||||
SelectorWithWidgetPreference preference = new SelectorWithWidgetPreference(mContext);
|
.setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(false).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
mController.updateState(mPreference, zenMode);
|
||||||
|
|
||||||
|
verifyNoMoreInteractions(mSession);
|
||||||
|
verify(mHelperBackend, never()).getPackagesBypassingDnd(anyInt(), anyBoolean());
|
||||||
|
assertThat(String.valueOf(mPreference.getSummary())).isEqualTo("None");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void updateState_withPolicyAllowingPriorityChannels_triggersRebuild() {
|
||||||
// Create a zen mode that allows priority channels to breakthrough.
|
// Create a zen mode that allows priority channels to breakthrough.
|
||||||
ZenMode zenMode = createPriorityChannelsZenMode();
|
ZenMode zenMode = createPriorityChannelsZenMode();
|
||||||
|
|
||||||
@@ -209,21 +238,35 @@ public final class ZenModeAppsLinkPreferenceControllerTest {
|
|||||||
|
|
||||||
// Updates the preference with the zen mode. We expect that this causes the app session
|
// Updates the preference with the zen mode. We expect that this causes the app session
|
||||||
// to trigger a rebuild (and display a temporary text in the meantime).
|
// to trigger a rebuild (and display a temporary text in the meantime).
|
||||||
mController.updateZenMode(preference, zenMode);
|
mController.updateZenMode(mPreference, zenMode);
|
||||||
verify(mSession).rebuild(any(), any(), eq(false));
|
verify(mSession).rebuild(any(), any(), eq(false));
|
||||||
assertThat(String.valueOf(preference.getSummary())).isEqualTo("Calculating…");
|
assertThat(String.valueOf(mPreference.getSummary())).isEqualTo("Calculating…");
|
||||||
|
|
||||||
// Manually triggers the callback that will happen on rebuild.
|
// Manually triggers the callback that will happen on rebuild.
|
||||||
mController.mAppSessionCallbacks.onRebuildComplete(appEntries);
|
mController.mAppSessionCallbacks.onRebuildComplete(appEntries);
|
||||||
assertThat(String.valueOf(preference.getSummary())).isEqualTo("test can interrupt");
|
assertThat(String.valueOf(mPreference.getSummary())).isEqualTo("test can interrupt");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void updateState_withPolicyAllowingPriorityChannels_loadsIcons() {
|
||||||
|
ZenMode zenMode = createPriorityChannelsZenMode();
|
||||||
|
|
||||||
|
mController.updateState(mPreference, zenMode);
|
||||||
|
when(mHelperBackend.getPackagesBypassingDnd(anyInt(), anyBoolean()))
|
||||||
|
.thenReturn(ImmutableList.of("test1", "test2"));
|
||||||
|
ArrayList<ApplicationsState.AppEntry> appEntries = new ArrayList<>();
|
||||||
|
appEntries.add(createAppEntry("test1", mContext.getUserId()));
|
||||||
|
appEntries.add(createAppEntry("test2", mContext.getUserId()));
|
||||||
|
mController.mAppSessionCallbacks.onRebuildComplete(appEntries);
|
||||||
|
|
||||||
|
assertThat(mPreference.getIconViews()).hasSize(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testOnPackageListChangedTriggersRebuild() {
|
public void testOnPackageListChangedTriggersRebuild() {
|
||||||
SelectorWithWidgetPreference preference = new SelectorWithWidgetPreference(mContext);
|
|
||||||
// Create a zen mode that allows priority channels to breakthrough.
|
// Create a zen mode that allows priority channels to breakthrough.
|
||||||
ZenMode zenMode = createPriorityChannelsZenMode();
|
ZenMode zenMode = createPriorityChannelsZenMode();
|
||||||
mController.updateState(preference, zenMode);
|
mController.updateState(mPreference, zenMode);
|
||||||
verify(mSession).rebuild(any(), any(), eq(false));
|
verify(mSession).rebuild(any(), any(), eq(false));
|
||||||
|
|
||||||
mController.mAppSessionCallbacks.onPackageListChanged();
|
mController.mAppSessionCallbacks.onPackageListChanged();
|
||||||
@@ -232,10 +275,9 @@ public final class ZenModeAppsLinkPreferenceControllerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testOnLoadEntriesCompletedTriggersRebuild() {
|
public void testOnLoadEntriesCompletedTriggersRebuild() {
|
||||||
SelectorWithWidgetPreference preference = new SelectorWithWidgetPreference(mContext);
|
|
||||||
// Create a zen mode that allows priority channels to breakthrough.
|
// Create a zen mode that allows priority channels to breakthrough.
|
||||||
ZenMode zenMode = createPriorityChannelsZenMode();
|
ZenMode zenMode = createPriorityChannelsZenMode();
|
||||||
mController.updateState(preference, zenMode);
|
mController.updateState(mPreference, zenMode);
|
||||||
verify(mSession).rebuild(any(), any(), eq(false));
|
verify(mSession).rebuild(any(), any(), eq(false));
|
||||||
|
|
||||||
mController.mAppSessionCallbacks.onLoadEntriesCompleted();
|
mController.mAppSessionCallbacks.onLoadEntriesCompleted();
|
||||||
|
@@ -17,6 +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.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
@@ -25,8 +26,6 @@ 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 androidx.preference.Preference;
|
|
||||||
|
|
||||||
import com.android.settingslib.notification.modes.TestModeBuilder;
|
import com.android.settingslib.notification.modes.TestModeBuilder;
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
@@ -63,8 +62,11 @@ public final class ZenModeOtherLinkPreferenceControllerTest {
|
|||||||
@Test
|
@Test
|
||||||
@EnableFlags(Flags.FLAG_MODES_UI)
|
@EnableFlags(Flags.FLAG_MODES_UI)
|
||||||
public void testHasSummary() {
|
public void testHasSummary() {
|
||||||
Preference pref = mock(Preference.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));
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -17,6 +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.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
@@ -25,8 +26,6 @@ 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 androidx.preference.Preference;
|
|
||||||
|
|
||||||
import com.android.settingslib.notification.modes.TestModeBuilder;
|
import com.android.settingslib.notification.modes.TestModeBuilder;
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
@@ -63,8 +62,11 @@ public final class ZenModePeopleLinkPreferenceControllerTest {
|
|||||||
@Test
|
@Test
|
||||||
@EnableFlags(Flags.FLAG_MODES_UI)
|
@EnableFlags(Flags.FLAG_MODES_UI)
|
||||||
public void testHasSummary() {
|
public void testHasSummary() {
|
||||||
Preference pref = mock(Preference.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));
|
||||||
}
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user