Files
app_Settings/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java
Matías Hernández 50c954923f Support (non-editable) display of DND with a filter != PRIORITY
Whenever setInterruptionFilter() is called with NONE or ALARMS, the Do Not Disturb mode now shows the current policy (nothing allowed or alarms/media allowed), instead of the normal PRIORITY policy. This policy is read-only in the UI since it cannot be customized.

This should be, or at least become, pretty rare (with small exceptions, apps targeting V that call setInterruptionFilter() will use an implicit mode instead of changing the global zen mode, plus using these filters is quite nonstandard in itself).

Fixes: 361586248
Test: atest & manual (toggling DND via cmd shell)
Flag: android.app.modes_ui
Change-Id: If2439480235d30aa310ad8925341183b9761784c
2024-08-30 15:03:03 +02:00

232 lines
10 KiB
Java

/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.notification.modes;
import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE;
import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT;
import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_NONE;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED;
import static android.service.notification.ZenPolicy.STATE_ALLOW;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.pm.LauncherApps;
import android.graphics.drawable.Drawable;
import android.service.notification.ConversationChannelWrapper;
import android.service.notification.ZenPolicy;
import android.service.notification.ZenPolicy.ConversationSenders;
import android.service.notification.ZenPolicy.PeopleType;
import android.util.IconDrawableFactory;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settings.notification.modes.ZenHelperBackend.Contact;
import com.android.settingslib.notification.ConversationIconFactory;
import com.android.settingslib.notification.modes.ZenMode;
import com.google.common.base.Equivalence;
import com.google.common.collect.ImmutableList;
import java.util.Objects;
/**
* Preference with a link and summary about what calls and messages can break through the mode,
* and icons representing those people.
*/
class ZenModePeopleLinkPreferenceController extends AbstractZenModePreferenceController {
private final ZenModeSummaryHelper mSummaryHelper;
private final ZenHelperBackend mHelperBackend;
private final ConversationIconFactory mConversationIconFactory;
ZenModePeopleLinkPreferenceController(Context context, String key,
ZenHelperBackend helperBackend) {
this(context, key, helperBackend,
new ConversationIconFactory(context,
context.getSystemService(LauncherApps.class),
context.getPackageManager(),
IconDrawableFactory.newInstance(context, false),
context.getResources().getDimensionPixelSize(
R.dimen.zen_mode_circular_icon_diameter)));
}
@VisibleForTesting
ZenModePeopleLinkPreferenceController(Context context, String key,
ZenHelperBackend helperBackend, ConversationIconFactory conversationIconFactory) {
super(context, key);
mSummaryHelper = new ZenModeSummaryHelper(mContext, helperBackend);
mHelperBackend = helperBackend;
mConversationIconFactory = conversationIconFactory;
}
@Override
public boolean isAvailable(ZenMode zenMode) {
return zenMode.getInterruptionFilter() != INTERRUPTION_FILTER_ALL;
}
@Override
public void updateState(Preference preference, @NonNull ZenMode zenMode) {
// Passes in source ZenModeFragment metric category.
preference.setIntent(
ZenSubSettingLauncher.forModeFragment(mContext, ZenModePeopleFragment.class,
zenMode.getId(), SettingsEnums.ZEN_PRIORITY_MODE).toIntent());
preference.setEnabled(zenMode.isEnabled() && zenMode.canEditPolicy());
preference.setSummary(mSummaryHelper.getPeopleSummary(zenMode.getPolicy()));
((CircularIconsPreference) preference).setIcons(getPeopleIcons(zenMode.getPolicy()),
PEOPLE_ITEM_EQUIVALENCE);
}
// Represents "Either<All, Contact, ConversationChannelWrapper>".
private record PeopleItem(boolean all,
@Nullable Contact contact,
@Nullable ConversationChannelWrapper conversation) {
private static final PeopleItem ALL = new PeopleItem(true, null, null);
PeopleItem(@NonNull Contact contact) {
this(false, contact, null);
}
PeopleItem(@NonNull ConversationChannelWrapper conversation) {
this(false, null, conversation);
}
}
private static final Equivalence<PeopleItem> PEOPLE_ITEM_EQUIVALENCE = new Equivalence<>() {
@Override
protected boolean doEquivalent(@NonNull PeopleItem a, @NonNull PeopleItem b) {
if (a.all && b.all) {
return true;
} else if (a.contact != null && b.contact != null) {
return a.contact.equals(b.contact);
} else if (a.conversation != null && b.conversation != null) {
ConversationChannelWrapper c1 = a.conversation;
ConversationChannelWrapper c2 = b.conversation;
// Skip comparing ShortcutInfo which doesn't implement equals(). We assume same
// conversation channel means same icon (which is not 100% correct but unlikely to
// change while on this screen).
return Objects.equals(c1.getNotificationChannel(), c2.getNotificationChannel())
&& Objects.equals(c1.getGroupLabel(), c2.getGroupLabel())
&& Objects.equals(c1.getParentChannelLabel(), c2.getParentChannelLabel())
&& Objects.equals(c1.getPkg(), c2.getPkg())
&& Objects.equals(c1.getUid(), c2.getUid());
} else {
return false;
}
}
@Override
protected int doHash(@NonNull PeopleItem item) {
return Objects.hash(item.all, item.contact, item.conversation);
}
};
private CircularIconSet<PeopleItem> getPeopleIcons(ZenPolicy policy) {
if (getCallersOrMessagesAllowed(policy) == PEOPLE_TYPE_ANYONE) {
return new CircularIconSet<>(
ImmutableList.of(PeopleItem.ALL),
this::loadPeopleIcon);
}
ImmutableList.Builder<PeopleItem> peopleItems = ImmutableList.builder();
fetchContactsAllowed(policy, peopleItems);
fetchConversationsAllowed(policy, peopleItems);
return new CircularIconSet<>(peopleItems.build(), this::loadPeopleIcon);
}
/**
* Adds {@link PeopleItem} entries corresponding to the set of people (contacts) who can
* break through via either call OR message.
*/
private void fetchContactsAllowed(ZenPolicy policy,
ImmutableList.Builder<PeopleItem> peopleItems) {
@PeopleType int peopleAllowed = getCallersOrMessagesAllowed(policy);
ImmutableList<Contact> contactsAllowed = ImmutableList.of();
if (peopleAllowed == PEOPLE_TYPE_CONTACTS) {
contactsAllowed = mHelperBackend.getAllContacts();
} else if (peopleAllowed == PEOPLE_TYPE_STARRED) {
contactsAllowed = mHelperBackend.getStarredContacts();
}
for (Contact contact : contactsAllowed) {
peopleItems.add(new PeopleItem(contact));
}
}
/**
* Adds {@link PeopleItem} entries corresponding to the set of conversation channels that can
* break through.
*/
private void fetchConversationsAllowed(ZenPolicy policy,
ImmutableList.Builder<PeopleItem> peopleItems) {
@ConversationSenders int conversationSendersAllowed =
policy.getPriorityCategoryConversations() == STATE_ALLOW
? policy.getPriorityConversationSenders()
: CONVERSATION_SENDERS_NONE;
ImmutableList<ConversationChannelWrapper> conversationsAllowed = ImmutableList.of();
if (conversationSendersAllowed == CONVERSATION_SENDERS_ANYONE) {
conversationsAllowed = mHelperBackend.getAllConversations();
} else if (conversationSendersAllowed == CONVERSATION_SENDERS_IMPORTANT) {
conversationsAllowed = mHelperBackend.getImportantConversations();
}
for (ConversationChannelWrapper conversation : conversationsAllowed) {
peopleItems.add(new PeopleItem(conversation));
}
}
/** Returns the broadest set of people who can call OR message. */
private @PeopleType int getCallersOrMessagesAllowed(ZenPolicy policy) {
@PeopleType int callersAllowed = policy.getPriorityCategoryCalls() == STATE_ALLOW
? policy.getPriorityCallSenders() : PEOPLE_TYPE_NONE;
@PeopleType int messagesAllowed = policy.getPriorityCategoryMessages() == STATE_ALLOW
? policy.getPriorityMessageSenders() : PEOPLE_TYPE_NONE;
// Order is ANYONE -> CONTACTS -> STARRED -> NONE, so just taking the minimum works.
return Math.min(callersAllowed, messagesAllowed);
}
@WorkerThread
private Drawable loadPeopleIcon(PeopleItem peopleItem) {
if (peopleItem.all) {
return IconUtil.makeCircularIconPreferenceItem(mContext,
R.drawable.ic_zen_mode_people_all);
} else if (peopleItem.contact != null) {
return mHelperBackend.getContactPhoto(peopleItem.contact);
} else if (peopleItem.conversation != null) {
return mConversationIconFactory.getConversationDrawable(
peopleItem.conversation.getShortcutInfo(),
peopleItem.conversation.getPkg(),
peopleItem.conversation.getUid(),
peopleItem.conversation.getNotificationChannel().isImportantConversation());
} else {
throw new IllegalArgumentException("Neither all nor contact nor conversation!");
}
}
}