Merge "Show icons for apps bypassing DND" into main
This commit is contained in:
committed by
Android (Google) Code Review
commit
bc0b3342bd
@@ -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 androidx.annotation.NonNull;
|
||||
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -42,8 +45,10 @@ class FutureUtil {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable throwable) {
|
||||
Log.e(TAG, String.format(errorLogMessage, errorLogMessageArgs), throwable);
|
||||
public void onFailure(@NonNull Throwable throwable) {
|
||||
if (!(throwable instanceof CancellationException)) {
|
||||
Log.e(TAG, String.format(errorLogMessage, errorLogMessageArgs), throwable);
|
||||
}
|
||||
}
|
||||
}, executor);
|
||||
}
|
||||
|
@@ -24,6 +24,7 @@ import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
import android.service.notification.ZenPolicy;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -32,6 +33,7 @@ import androidx.fragment.app.Fragment;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.Utils;
|
||||
import com.android.settings.core.SubSettingLauncher;
|
||||
import com.android.settingslib.applications.ApplicationsState;
|
||||
import com.android.settingslib.applications.ApplicationsState.AppEntry;
|
||||
@@ -59,7 +61,7 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr
|
||||
private ApplicationsState.Session mAppSession;
|
||||
private final ZenHelperBackend mHelperBackend;
|
||||
private ZenMode mZenMode;
|
||||
private Preference mPreference;
|
||||
private CircularIconsPreference mPreference;
|
||||
private final Fragment mHost;
|
||||
|
||||
ZenModeAppsLinkPreferenceController(Context context, String key, Fragment host,
|
||||
@@ -97,14 +99,21 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr
|
||||
.setArguments(bundle)
|
||||
.toIntent());
|
||||
mZenMode = zenMode;
|
||||
mPreference = preference;
|
||||
if (TextUtils.isEmpty(mPreference.getSummary())) {
|
||||
mPreference.setSummary(R.string.zen_mode_apps_calculating);
|
||||
mPreference = (CircularIconsPreference) preference;
|
||||
|
||||
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() {
|
||||
@@ -126,6 +135,9 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr
|
||||
ImmutableList<AppEntry> apps = getAppsBypassingDndSortedByName(allApps);
|
||||
|
||||
mPreference.setSummary(mSummaryHelper.getAppsSummary(mZenMode, apps));
|
||||
|
||||
mPreference.displayIcons(new CircularIconSet<>(apps,
|
||||
app -> Utils.getBadgedIcon(mContext, app.info)));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
@@ -17,15 +17,12 @@
|
||||
package com.android.settings.notification.modes;
|
||||
|
||||
import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
|
||||
import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.android.settings.core.SubSettingLauncher;
|
||||
import com.android.settingslib.notification.modes.ZenMode;
|
||||
|
||||
/**
|
||||
@@ -48,13 +45,13 @@ class ZenModeOtherLinkPreferenceController extends AbstractZenModePreferenceCont
|
||||
|
||||
@Override
|
||||
public void updateState(Preference preference, @NonNull ZenMode zenMode) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, zenMode.getId());
|
||||
preference.setIntent(new SubSettingLauncher(mContext)
|
||||
.setDestination(ZenModeOtherFragment.class.getName())
|
||||
.setSourceMetricsCategory(0)
|
||||
.setArguments(bundle)
|
||||
.toIntent());
|
||||
// TODO: b/332937635 - Update metrics category
|
||||
preference.setIntent(
|
||||
ZenSubSettingLauncher.forModeFragment(mContext, ZenModeOtherFragment.class,
|
||||
zenMode.getId(), 0).toIntent());
|
||||
|
||||
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;
|
||||
|
||||
import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
|
||||
import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.android.settings.core.SubSettingLauncher;
|
||||
import com.android.settingslib.notification.modes.ZenMode;
|
||||
|
||||
/**
|
||||
@@ -48,14 +45,13 @@ class ZenModePeopleLinkPreferenceController extends AbstractZenModePreferenceCon
|
||||
|
||||
@Override
|
||||
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
|
||||
preference.setIntent(new SubSettingLauncher(mContext)
|
||||
.setDestination(ZenModePeopleFragment.class.getName())
|
||||
.setSourceMetricsCategory(0)
|
||||
.setArguments(bundle)
|
||||
.toIntent());
|
||||
preference.setIntent(
|
||||
ZenSubSettingLauncher.forModeFragment(mContext, ZenModePeopleFragment.class,
|
||||
zenMode.getId(), 0).toIntent());
|
||||
|
||||
preference.setSummary(mSummaryHelper.getPeopleSummary(zenMode));
|
||||
// TODO: b/346551087 - Show people icons
|
||||
((CircularIconsPreference) preference).displayIcons(CircularIconSet.EMPTY);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user