Show icons for allowed contacts

Test: atest com.android.settings.notification.modes
Bug: 346551087
Flag: android.app.modes_ui
Change-Id: If2b6b06b4a9c16bdefb03850ad1615e96c601fbd
This commit is contained in:
Matías Hernández
2024-07-21 00:47:31 +02:00
parent 5c466c5ba3
commit d8b9fe8f01
13 changed files with 559 additions and 97 deletions

View File

@@ -0,0 +1,25 @@
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/colorControlNormal"
android:viewportHeight="960"
android:viewportWidth="960">
<path
android:fillColor="@android:color/white"
android:pathData="M480,480Q414,480 367,433Q320,386 320,320Q320,254 367,207Q414,160 480,160Q546,160 593,207Q640,254 640,320Q640,386 593,433Q546,480 480,480ZM160,800L160,688Q160,654 177.5,625.5Q195,597 224,582Q286,551 350,535.5Q414,520 480,520Q546,520 610,535.5Q674,551 736,582Q765,597 782.5,625.5Q800,654 800,688L800,800L160,800ZM240,720L720,720L720,688Q720,677 714.5,668Q709,659 700,654Q646,627 591,613.5Q536,600 480,600Q424,600 369,613.5Q314,627 260,654Q251,659 245.5,668Q240,677 240,688L240,720ZM480,400Q513,400 536.5,376.5Q560,353 560,320Q560,287 536.5,263.5Q513,240 480,240Q447,240 423.5,263.5Q400,287 400,320Q400,353 423.5,376.5Q447,400 480,400ZM480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320ZM480,720L480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720L480,720Z" />
</vector>

View File

@@ -0,0 +1,25 @@
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/colorControlNormal"
android:viewportHeight="960"
android:viewportWidth="960">
<path
android:fillColor="@android:color/white"
android:pathData="M0,720L0,657Q0,614 44,587Q88,560 160,560Q173,560 185,560.5Q197,561 208,563Q194,584 187,607Q180,630 180,655L180,720L0,720ZM240,720L240,655Q240,623 257.5,596.5Q275,570 307,550Q339,530 383.5,520Q428,510 480,510Q533,510 577.5,520Q622,530 654,550Q686,570 703,596.5Q720,623 720,655L720,720L240,720ZM780,720L780,655Q780,629 773.5,606Q767,583 754,563Q765,561 776.5,560.5Q788,560 800,560Q872,560 916,586.5Q960,613 960,657L960,720L780,720ZM325,640L636,640L636,640Q626,620 580.5,605Q535,590 480,590Q425,590 379.5,605Q334,620 325,640ZM160,520Q127,520 103.5,496.5Q80,473 80,440Q80,406 103.5,383Q127,360 160,360Q194,360 217,383Q240,406 240,440Q240,473 217,496.5Q194,520 160,520ZM800,520Q767,520 743.5,496.5Q720,473 720,440Q720,406 743.5,383Q767,360 800,360Q834,360 857,383Q880,406 880,440Q880,473 857,496.5Q834,520 800,520ZM480,480Q430,480 395,445Q360,410 360,360Q360,309 395,274.5Q430,240 480,240Q531,240 565.5,274.5Q600,309 600,360Q600,410 565.5,445Q531,480 480,480ZM480,400Q497,400 508.5,388.5Q520,377 520,360Q520,343 508.5,331.5Q497,320 480,320Q463,320 451.5,331.5Q440,343 440,360Q440,377 451.5,388.5Q463,400 480,400ZM481,640L481,640Q481,640 481,640Q481,640 481,640Q481,640 481,640Q481,640 481,640L481,640ZM480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Z" />
</vector>

View File

@@ -513,4 +513,5 @@
<dimen name="zen_mode_circular_icon_inner_icon_size">20dp</dimen> <dimen name="zen_mode_circular_icon_inner_icon_size">20dp</dimen>
<dimen name="zen_mode_circular_icon_margin_between">4dp</dimen> <dimen name="zen_mode_circular_icon_margin_between">4dp</dimen>
<dimen name="zen_mode_circular_icon_margin_vertical">8dp</dimen> <dimen name="zen_mode_circular_icon_margin_vertical">8dp</dimen>
<dimen name="zen_mode_circular_icon_text_size">18dp</dimen>
</resources> </resources>

View File

@@ -9424,6 +9424,8 @@
<!-- [CHAR LIMIT=120] Zen mode settings: Summary for people category --> <!-- [CHAR LIMIT=120] Zen mode settings: Summary for people category -->
<string name="zen_mode_people_some">Some people can interrupt</string> <string name="zen_mode_people_some">Some people can interrupt</string>
<!-- [CHAR LIMIT=120] Zen mode settings: Summary for people category --> <!-- [CHAR LIMIT=120] Zen mode settings: Summary for people category -->
<string name="zen_mode_people_repeat_callers">Repeat callers can interrupt</string>
<!-- [CHAR LIMIT=120] Zen mode settings: Summary for people category -->
<string name="zen_mode_people_all">All people can interrupt</string> <string name="zen_mode_people_all">All people can interrupt</string>
<!-- [CHAR LIMIT=50] Zen mode settings: Repeat callers option --> <!-- [CHAR LIMIT=50] Zen mode settings: Repeat callers option -->

View File

@@ -20,6 +20,13 @@ import static com.google.common.base.Preconditions.checkNotNull;
import android.content.Context; import android.content.Context;
import android.content.res.ColorStateList; import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable; import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.ShapeDrawable;
@@ -29,11 +36,16 @@ import android.view.Gravity;
import androidx.annotation.AttrRes; import androidx.annotation.AttrRes;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px; import androidx.annotation.Px;
import com.android.settings.R; import com.android.settings.R;
import com.android.settingslib.Utils; import com.android.settingslib.Utils;
import com.google.common.base.Strings;
import java.util.Locale;
class IconUtil { class IconUtil {
static Drawable applyNormalTint(@NonNull Context context, @NonNull Drawable icon) { static Drawable applyNormalTint(@NonNull Context context, @NonNull Drawable icon) {
@@ -87,9 +99,10 @@ class IconUtil {
/** /**
* Returns a variant of the supplied icon to be used in a {@link CircularIconsPreference}. The * Returns a variant of the supplied icon to be used in a {@link CircularIconsPreference}. The
* inner icon is 20x20 dp and it's contained in a circle of diameter 32dp, and is tinted * inner icon is 20x20 dp and it's contained in a circle of diameter 32dp, and is tinted
* with the "material secondary container" color combination. * with the "material secondary" color combination.
*/ */
static Drawable makeSoundIcon(@NonNull Context context, @DrawableRes int iconResId) { static Drawable makeCircularIconPreferenceItem(@NonNull Context context,
@DrawableRes int iconResId) {
return composeIconCircle( return composeIconCircle(
Utils.getColorAttr(context, Utils.getColorAttr(context,
com.android.internal.R.attr.materialColorSecondaryContainer), com.android.internal.R.attr.materialColorSecondaryContainer),
@@ -102,6 +115,53 @@ class IconUtil {
R.dimen.zen_mode_circular_icon_inner_icon_size)); R.dimen.zen_mode_circular_icon_inner_icon_size));
} }
/**
* Returns an icon representing a contact that doesn't have an associated photo, to be used in
* a {@link CircularIconsPreference}, tinted with the "material tertiary". If the contact's
* display name is not empty, it's the contact's monogram, otherwise it's a generic icon.
*/
static Drawable makeContactMonogram(@NonNull Context context, @Nullable String displayName) {
Resources res = context.getResources();
if (Strings.isNullOrEmpty(displayName)) {
return composeIconCircle(
Utils.getColorAttr(context,
com.android.internal.R.attr.materialColorTertiaryContainer),
res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_diameter),
checkNotNull(context.getDrawable(R.drawable.ic_zen_mode_generic_contact)),
Utils.getColorAttr(context,
com.android.internal.R.attr.materialColorOnTertiaryContainer),
res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_inner_icon_size));
}
float diameter = res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_diameter);
Bitmap bitmap = Bitmap.createBitmap((int) diameter, (int) diameter,
Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint circlePaint = new Paint();
circlePaint.setColor(Utils.getColorAttrDefaultColor(context,
com.android.internal.R.attr.materialColorTertiaryContainer));
circlePaint.setFlags(Paint.ANTI_ALIAS_FLAG);
canvas.drawCircle(diameter / 2f, diameter / 2f, diameter / 2f, circlePaint);
Paint textPaint = new Paint();
textPaint.setColor(Utils.getColorAttrDefaultColor(context,
com.android.internal.R.attr.materialColorOnTertiaryContainer));
textPaint.setTypeface(Typeface.create("sans-serif", Typeface.NORMAL));
textPaint.setTextAlign(Paint.Align.LEFT);
textPaint.setTextSize(res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_text_size));
String text = displayName.substring(0, 1).toUpperCase(Locale.getDefault());
Rect textRect = new Rect();
textPaint.getTextBounds(text, 0, text.length(), textRect);
float textX = diameter / 2f - textRect.width() / 2f - textRect.left;
float textY = diameter / 2f + textRect.height() / 2f - textRect.bottom;
canvas.drawText(text, textX, textY, textPaint);
return new BitmapDrawable(context.getResources(), bitmap);
}
private static Drawable composeIconCircle(ColorStateList circleColor, @Px int circleDiameterPx, private static Drawable composeIconCircle(ColorStateList circleColor, @Px int circleDiameterPx,
Drawable icon, ColorStateList iconColor, @Px int iconSizePx) { Drawable icon, ColorStateList iconColor, @Px int iconSizePx) {
ShapeDrawable background = new ShapeDrawable(new OvalShape()); ShapeDrawable background = new ShapeDrawable(new OvalShape());

View File

@@ -21,15 +21,22 @@ import android.app.INotificationManager;
import android.content.Context; import android.content.Context;
import android.content.pm.ParceledListSlice; import android.content.pm.ParceledListSlice;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.ServiceManager; import android.os.ServiceManager;
import android.provider.ContactsContract; import android.provider.ContactsContract;
import android.service.notification.ConversationChannelWrapper; import android.service.notification.ConversationChannelWrapper;
import android.util.Log; import android.util.Log;
import androidx.annotation.VisibleForTesting; import androidx.annotation.NonNull;
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
import com.android.settings.R; import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -75,45 +82,99 @@ class ZenHelperBackend {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
ParceledListSlice<ConversationChannelWrapper> getConversations(boolean onlyImportant) { ImmutableList<ConversationChannelWrapper> getImportantConversations() {
try { try {
return mInm.getConversations(onlyImportant); ImmutableList.Builder<ConversationChannelWrapper> list = new ImmutableList.Builder<>();
ParceledListSlice<ConversationChannelWrapper> parceledList = mInm.getConversations(
/* onlyImportant= */ true);
if (parceledList != null) {
for (ConversationChannelWrapper conversation : parceledList.getList()) {
if (!conversation.getNotificationChannel().isDemoted()) {
list.add(conversation);
}
}
}
return list.build();
} catch (Exception e) { } catch (Exception e) {
Log.w(TAG, "Error calling NoMan", e); Log.w(TAG, "Error calling NoMan", e);
return ParceledListSlice.emptyList(); return ImmutableList.of();
} }
} }
List<String> getStarredContacts() { record Contact(long id, @Nullable String displayName, @Nullable Uri photoUri) { }
ImmutableList<Contact> getAllContacts() {
try (Cursor cursor = queryAllContactsData()) {
return getContactsFromCursor(cursor);
}
}
ImmutableList<Contact> getStarredContacts() {
try (Cursor cursor = queryStarredContactsData()) { try (Cursor cursor = queryStarredContactsData()) {
return getStarredContacts(cursor); return getContactsFromCursor(cursor);
} }
} }
@VisibleForTesting private ImmutableList<Contact> getContactsFromCursor(Cursor cursor) {
List<String> getStarredContacts(Cursor cursor) { ImmutableList.Builder<Contact> list = new ImmutableList.Builder<>();
List<String> starredContacts = new ArrayList<>();
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
do { do {
String contact = cursor.getString(0); long id = cursor.getLong(0);
starredContacts.add(contact != null ? contact : String name = Strings.emptyToNull(cursor.getString(1));
mContext.getString(R.string.zen_mode_starred_contacts_empty_name)); String photoUriStr = cursor.getString(2);
Uri photoUri = !Strings.isNullOrEmpty(photoUriStr) ? Uri.parse(photoUriStr) : null;
list.add(new Contact(id, name, photoUri));
} while (cursor.moveToNext()); } while (cursor.moveToNext());
} }
return starredContacts; return list.build();
} }
int getAllContactsCount() {
try (Cursor cursor = queryAllContactsData()) {
return cursor != null ? cursor.getCount() : 0;
}
}
private static final String[] CONTACTS_PROJECTION = new String[] {
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME_PRIMARY,
ContactsContract.Contacts.PHOTO_THUMBNAIL_URI
};
private Cursor queryStarredContactsData() { private Cursor queryStarredContactsData() {
return mContext.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, return mContext.getContentResolver().query(
new String[]{ContactsContract.Contacts.DISPLAY_NAME_PRIMARY}, ContactsContract.Contacts.CONTENT_URI,
ContactsContract.Data.STARRED + "=1", null, CONTACTS_PROJECTION,
ContactsContract.Data.TIMES_CONTACTED); /* selection= */ ContactsContract.Data.STARRED + "=1", /* selectionArgs= */ null,
/* sortOrder= */ ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
} }
Cursor queryAllContactsData() { private Cursor queryAllContactsData() {
return mContext.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, return mContext.getContentResolver().query(
new String[]{ContactsContract.Contacts.DISPLAY_NAME_PRIMARY}, ContactsContract.Contacts.CONTENT_URI,
null, null, null); CONTACTS_PROJECTION,
/* selection= */ null, /* selectionArgs= */ null,
/* sortOrder= */ ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
}
@NonNull
Drawable getContactPhoto(Contact contact) {
if (contact.photoUri != null) {
try (InputStream is = mContext.getContentResolver().openInputStream(contact.photoUri)) {
if (is != null) {
RoundedBitmapDrawable rbd = RoundedBitmapDrawableFactory.create(
mContext.getResources(), is);
if (rbd != null && rbd.getBitmap() != null) {
rbd.setCircular(true);
return rbd;
}
}
} catch (IOException e) {
Log.w(TAG, "Couldn't load photo for " + contact, e);
}
}
// Fall back to a monogram if no picture.
return IconUtil.makeContactMonogram(mContext, contact.displayName);
} }
} }

View File

@@ -87,6 +87,6 @@ class ZenModeOtherLinkPreferenceController extends AbstractZenModePreferenceCont
} }
} }
return new CircularIconSet<>(icons.build(), return new CircularIconSet<>(icons.build(),
iconResId -> IconUtil.makeSoundIcon(mContext, iconResId)); iconResId -> IconUtil.makeCircularIconPreferenceItem(mContext, iconResId));
} }
} }

View File

@@ -17,25 +17,67 @@
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.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.content.Context; 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.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.preference.Preference; 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.android.settingslib.notification.modes.ZenMode;
import com.google.common.collect.ImmutableList;
import java.util.function.Function;
/** /**
* Preference with a link and summary about what calls and messages can break through the mode * 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 { class ZenModePeopleLinkPreferenceController extends AbstractZenModePreferenceController {
private final ZenModeSummaryHelper mSummaryHelper; private final ZenModeSummaryHelper mSummaryHelper;
private final ZenHelperBackend mHelperBackend;
private final ConversationIconFactory mConversationIconFactory;
public ZenModePeopleLinkPreferenceController(Context context, String key, ZenModePeopleLinkPreferenceController(Context context, String key,
ZenHelperBackend helperBackend) { 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); super(context, key);
mSummaryHelper = new ZenModeSummaryHelper(mContext, helperBackend); mSummaryHelper = new ZenModeSummaryHelper(mContext, helperBackend);
mHelperBackend = helperBackend;
mConversationIconFactory = conversationIconFactory;
} }
@Override @Override
@@ -50,8 +92,104 @@ class ZenModePeopleLinkPreferenceController extends AbstractZenModePreferenceCon
ZenSubSettingLauncher.forModeFragment(mContext, ZenModePeopleFragment.class, ZenSubSettingLauncher.forModeFragment(mContext, ZenModePeopleFragment.class,
zenMode.getId(), 0).toIntent()); zenMode.getId(), 0).toIntent());
preference.setSummary(mSummaryHelper.getPeopleSummary(zenMode)); preference.setSummary(mSummaryHelper.getPeopleSummary(zenMode.getPolicy()));
// TODO: b/346551087 - Show people icons ((CircularIconsPreference) preference).displayIcons(getPeopleIcons(zenMode.getPolicy()));
((CircularIconsPreference) preference).displayIcons(CircularIconSet.EMPTY); }
// Represents "Either<Contact, ConversationChannelWrapper>".
record PeopleItem(@Nullable Contact contact,
@Nullable ConversationChannelWrapper conversation) {
PeopleItem(@NonNull Contact contact) {
this(contact, null);
}
PeopleItem(@NonNull ConversationChannelWrapper conversation) {
this(null, conversation);
}
}
private CircularIconSet<?> getPeopleIcons(ZenPolicy policy) {
if (getCallersOrMessagesAllowed(policy) == PEOPLE_TYPE_ANYONE) {
return new CircularIconSet<>(
ImmutableList.of(IconUtil.makeCircularIconPreferenceItem(mContext,
R.drawable.ic_zen_mode_people_all)),
Function.identity());
}
ImmutableList.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) {
// TODO: b/354658240 - Need to handle CONVERSATION_SENDERS_ANYONE?
return;
} 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.contact != null) {
return mHelperBackend.getContactPhoto(peopleItem.contact);
} else if (peopleItem.conversation != null) {
return mConversationIconFactory.getConversationDrawable(
peopleItem.conversation.getShortcutInfo(),
peopleItem.conversation.getPkg(),
peopleItem.conversation.getUid(),
/* important= */ true);
} else {
throw new IllegalArgumentException("Neither contact nor conversation!");
}
} }
} }

View File

@@ -30,10 +30,8 @@ import android.app.settings.SettingsEnums;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
import android.icu.text.MessageFormat; import android.icu.text.MessageFormat;
import android.provider.Contacts; import android.provider.Contacts;
import android.service.notification.ConversationChannelWrapper;
import android.service.notification.ZenPolicy; import android.service.notification.ZenPolicy;
import android.view.View; import android.view.View;
@@ -167,17 +165,7 @@ class ZenModePrioritySendersPreferenceController
} }
private void updateChannelCounts() { private void updateChannelCounts() {
ParceledListSlice<ConversationChannelWrapper> impConversations = mNumImportantConversations = mHelperBackend.getImportantConversations().size();
mHelperBackend.getConversations(true);
int numImportantConversations = 0;
if (impConversations != null) {
for (ConversationChannelWrapper conversation : impConversations.getList()) {
if (!conversation.getNotificationChannel().isDemoted()) {
numImportantConversations++;
}
}
}
mNumImportantConversations = numImportantConversations;
} }
private int getPrioritySenders(ZenPolicy policy) { private int getPrioritySenders(ZenPolicy policy) {

View File

@@ -31,6 +31,7 @@ import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_MESSAGES;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REMINDERS; import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REMINDERS;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REPEAT_CALLERS; import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REPEAT_CALLERS;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_SYSTEM; import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_SYSTEM;
import static android.service.notification.ZenPolicy.STATE_ALLOW;
import static android.service.notification.ZenPolicy.VISUAL_EFFECT_AMBIENT; import static android.service.notification.ZenPolicy.VISUAL_EFFECT_AMBIENT;
import static android.service.notification.ZenPolicy.VISUAL_EFFECT_BADGE; import static android.service.notification.ZenPolicy.VISUAL_EFFECT_BADGE;
import static android.service.notification.ZenPolicy.VISUAL_EFFECT_FULL_SCREEN_INTENT; import static android.service.notification.ZenPolicy.VISUAL_EFFECT_FULL_SCREEN_INTENT;
@@ -45,6 +46,8 @@ import android.provider.Settings;
import android.service.notification.ZenDeviceEffects; import android.service.notification.ZenDeviceEffects;
import android.service.notification.ZenModeConfig; import android.service.notification.ZenModeConfig;
import android.service.notification.ZenPolicy; import android.service.notification.ZenPolicy;
import android.service.notification.ZenPolicy.ConversationSenders;
import android.service.notification.ZenPolicy.PeopleType;
import android.util.ArrayMap; import android.util.ArrayMap;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -56,6 +59,7 @@ import com.android.settings.R;
import com.android.settingslib.applications.ApplicationsState.AppEntry; import com.android.settingslib.applications.ApplicationsState.AppEntry;
import com.android.settingslib.notification.modes.ZenMode; import com.android.settingslib.notification.modes.ZenMode;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.ArrayList; import java.util.ArrayList;
@@ -365,7 +369,12 @@ class ZenModeSummaryHelper {
} }
public String getStarredContactsSummary() { public String getStarredContactsSummary() {
List<String> starredContacts = mBackend.getStarredContacts(); List<String> starredContacts = mBackend.getStarredContacts().stream()
.map(ZenHelperBackend.Contact::displayName)
.map(name -> Strings.isNullOrEmpty(name)
? mContext.getString(R.string.zen_mode_starred_contacts_empty_name)
: name)
.toList();
int numStarredContacts = starredContacts.size(); int numStarredContacts = starredContacts.size();
MessageFormat msgFormat = new MessageFormat( MessageFormat msgFormat = new MessageFormat(
mContext.getString(R.string.zen_mode_starred_contacts_summary_contacts), mContext.getString(R.string.zen_mode_starred_contacts_summary_contacts),
@@ -389,26 +398,32 @@ class ZenModeSummaryHelper {
mContext.getString(R.string.zen_mode_contacts_count), mContext.getString(R.string.zen_mode_contacts_count),
Locale.getDefault()); Locale.getDefault());
Map<String, Object> args = new HashMap<>(); Map<String, Object> args = new HashMap<>();
args.put("count", mBackend.queryAllContactsData().getCount()); args.put("count", mBackend.getAllContactsCount());
return msgFormat.format(args); return msgFormat.format(args);
} }
public String getPeopleSummary(ZenMode zenMode) { public String getPeopleSummary(ZenPolicy policy) {
final int callersAllowed = zenMode.getPolicy().getPriorityCallSenders(); @PeopleType int callersAllowed = policy.getPriorityCategoryCalls() == STATE_ALLOW
final int messagesAllowed = zenMode.getPolicy().getPriorityMessageSenders(); ? policy.getPriorityCallSenders() : PEOPLE_TYPE_NONE;
final int conversationsAllowed = zenMode.getPolicy().getPriorityConversationSenders(); @PeopleType int messagesAllowed = policy.getPriorityCategoryMessages() == STATE_ALLOW
? policy.getPriorityMessageSenders() : PEOPLE_TYPE_NONE;
@ConversationSenders int conversationsAllowed =
policy.getPriorityCategoryConversations() == STATE_ALLOW
? policy.getPriorityConversationSenders()
: CONVERSATION_SENDERS_NONE;
final boolean areRepeatCallersAllowed = final boolean areRepeatCallersAllowed =
zenMode.getPolicy().isCategoryAllowed(PRIORITY_CATEGORY_REPEAT_CALLERS, false); policy.isCategoryAllowed(PRIORITY_CATEGORY_REPEAT_CALLERS, false);
if (callersAllowed == PEOPLE_TYPE_ANYONE if (callersAllowed == PEOPLE_TYPE_ANYONE
&& messagesAllowed == PEOPLE_TYPE_ANYONE && messagesAllowed == PEOPLE_TYPE_ANYONE
&& conversationsAllowed == CONVERSATION_SENDERS_ANYONE) { && conversationsAllowed == CONVERSATION_SENDERS_ANYONE) {
return mContext.getResources().getString(R.string.zen_mode_people_all); return mContext.getString(R.string.zen_mode_people_all);
} else if (callersAllowed == PEOPLE_TYPE_NONE } else if (callersAllowed == PEOPLE_TYPE_NONE
&& messagesAllowed == PEOPLE_TYPE_NONE && messagesAllowed == PEOPLE_TYPE_NONE
&& conversationsAllowed == CONVERSATION_SENDERS_NONE && conversationsAllowed == CONVERSATION_SENDERS_NONE) {
&& !areRepeatCallersAllowed) { return mContext.getString(
return mContext.getResources().getString(R.string.zen_mode_people_none); areRepeatCallersAllowed ? R.string.zen_mode_people_repeat_callers
: R.string.zen_mode_people_none);
} else { } else {
return mContext.getResources().getString(R.string.zen_mode_people_some); return mContext.getResources().getString(R.string.zen_mode_people_some);
} }

View File

@@ -16,17 +16,47 @@
package com.android.settings.notification.modes; package com.android.settings.notification.modes;
import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT;
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 com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.mock; import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.Flags; import android.app.Flags;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context; import android.content.Context;
import android.content.pm.ShortcutInfo;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
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.ConversationChannelWrapper;
import android.service.notification.ZenPolicy;
import android.view.LayoutInflater;
import android.view.View;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.notification.modes.ZenHelperBackend.Contact;
import com.android.settingslib.notification.ConversationIconFactory;
import com.android.settingslib.notification.modes.TestModeBuilder; import com.android.settingslib.notification.modes.TestModeBuilder;
import com.android.settingslib.notification.modes.ZenMode;
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;
@@ -34,39 +64,160 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.mockito.stubbing.Answer;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment; import org.robolectric.RuntimeEnvironment;
import java.util.Collection;
@EnableFlags(Flags.FLAG_MODES_UI)
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
public final class ZenModePeopleLinkPreferenceControllerTest { public final class ZenModePeopleLinkPreferenceControllerTest {
private ZenModePeopleLinkPreferenceController mController; private ZenModePeopleLinkPreferenceController mController;
private CircularIconsPreference mPreference;
@Rule @Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private Context mContext; private Context mContext;
@Mock @Mock private ZenHelperBackend mHelperBackend;
private ZenHelperBackend mHelperBackend; @Mock private ConversationIconFactory mConversationIconFactory;
@Before @Before
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());
// Ensure the preference view is bound & measured (needed to add icons).
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);
mController = new ZenModePeopleLinkPreferenceController( mController = new ZenModePeopleLinkPreferenceController(
mContext, "something", mHelperBackend); mContext, "something", mHelperBackend, mConversationIconFactory);
setUpContacts(ImmutableList.of(), ImmutableList.of());
setUpImportantConversations(ImmutableList.of());
when(mHelperBackend.getContactPhoto(any())).then(
(Answer<Drawable>) invocationOnMock -> photoOf(invocationOnMock.getArgument(0)));
when(mConversationIconFactory.getConversationDrawable((ShortcutInfo) any(), any(), anyInt(),
anyBoolean())).thenReturn(new ColorDrawable(Color.BLACK));
} }
@Test @Test
@EnableFlags(Flags.FLAG_MODES_UI) public void updateState_setsSummary() {
public void testHasSummary() { mController.updateState(mPreference, TestModeBuilder.EXAMPLE);
CircularIconsPreference pref = mock(CircularIconsPreference.class);
mController.updateZenMode(pref, TestModeBuilder.EXAMPLE); assertThat(mPreference.getSummary()).isNotNull();
assertThat(mPreference.getSummary().toString()).isNotEmpty();
}
verify(pref).setSummary(any()); @Test
verify(pref).displayIcons(eq(CircularIconSet.EMPTY)); public void updateState_starredCallsNoMessages_displaysStarredContacts() {
setUpContacts(ImmutableList.of(1, 2, 3, 4), ImmutableList.of(2, 3));
ZenMode mode = new TestModeBuilder()
.setZenPolicy(new ZenPolicy.Builder()
.allowCalls(PEOPLE_TYPE_STARRED)
.allowMessages(PEOPLE_TYPE_NONE)
.build())
.build();
mController.updateState(mPreference, mode);
assertThat(mPreference.getIcons()).hasSize(2);
assertThat(mPreference.getIcons().stream()
.map(ColorDrawable.class::cast)
.map(d -> d.getColor()).toList())
.containsExactly(2, 3).inOrder();
}
@Test
public void updateState_starredCallsContactMessages_displaysAllContacts() {
setUpContacts(ImmutableList.of(1, 2, 3, 4), ImmutableList.of(2, 3));
ZenMode mode = new TestModeBuilder()
.setZenPolicy(new ZenPolicy.Builder()
.allowCalls(PEOPLE_TYPE_STARRED)
.allowMessages(PEOPLE_TYPE_CONTACTS)
.build())
.build();
mController.updateState(mPreference, mode);
assertThat(mPreference.getIcons()).hasSize(4);
assertThat(mPreference.getIcons().stream()
.map(ColorDrawable.class::cast)
.map(d -> d.getColor()).toList())
.containsExactly(1, 2, 3, 4).inOrder();
}
@Test
public void updateState_anyoneCallsContactMessages_displaysAnyonePlaceholder() {
setUpContacts(ImmutableList.of(1, 2, 3, 4), ImmutableList.of(2, 3));
ZenMode mode = new TestModeBuilder()
.setZenPolicy(new ZenPolicy.Builder()
.allowCalls(PEOPLE_TYPE_ANYONE)
.allowMessages(PEOPLE_TYPE_CONTACTS)
.build())
.build();
mController.updateState(mPreference, mode);
assertThat(mPreference.getIcons()).hasSize(1);
verify(mHelperBackend, never()).getContactPhoto(any());
}
@Test
public void updateState_noContactsButImportantConversations_displaysConversations() {
setUpContacts(ImmutableList.of(), ImmutableList.of());
setUpImportantConversations(ImmutableList.of(1, 2, 3));
ZenMode mode = new TestModeBuilder()
.setZenPolicy(new ZenPolicy.Builder()
.allowCalls(PEOPLE_TYPE_CONTACTS)
.allowMessages(PEOPLE_TYPE_CONTACTS)
.allowConversations(CONVERSATION_SENDERS_IMPORTANT)
.build())
.build();
mController.updateState(mPreference, mode);
assertThat(mPreference.getIcons()).hasSize(3);
verify(mConversationIconFactory, times(3)).getConversationDrawable((ShortcutInfo) any(),
any(), anyInt(), anyBoolean());
}
private void setUpContacts(Collection<Integer> allIds, Collection<Integer> starredIds) {
when(mHelperBackend.getAllContacts()).thenReturn(ImmutableList.copyOf(
allIds.stream()
.map(id -> new Contact(id, "#" + id, Uri.parse("photo://" + id)))
.toList()));
when(mHelperBackend.getStarredContacts()).thenReturn(ImmutableList.copyOf(
starredIds.stream()
.map(id -> new Contact(id, "#" + id, Uri.parse("photo://" + id)))
.toList()));
}
private void setUpImportantConversations(Collection<Integer> ids) {
when(mHelperBackend.getImportantConversations()).thenReturn(ImmutableList.copyOf(
ids.stream()
.map(id -> {
ConversationChannelWrapper channel = new ConversationChannelWrapper();
channel.setNotificationChannel(
new NotificationChannel(id.toString(), id.toString(),
NotificationManager.IMPORTANCE_DEFAULT));
return channel;
})
.toList()));
}
private static ColorDrawable photoOf(Contact contact) {
return new ColorDrawable((int) contact.id());
} }
} }

View File

@@ -35,13 +35,11 @@ import static com.android.settings.notification.modes.ZenModePrioritySendersPref
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import android.app.Flags; import android.app.Flags;
import android.content.Context; import android.content.Context;
import android.database.Cursor;
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;
@@ -56,6 +54,8 @@ 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.android.settingslib.widget.SelectorWithWidgetPreference;
import com.google.common.collect.ImmutableList;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
@@ -105,22 +105,11 @@ public final class ZenModePrioritySendersPreferenceControllerTest {
mPreferenceScreen.addPreference(mCallsPrefCategory); mPreferenceScreen.addPreference(mCallsPrefCategory);
mPreferenceScreen.addPreference(mMessagesPrefCategory); mPreferenceScreen.addPreference(mMessagesPrefCategory);
Cursor cursor = mock(Cursor.class); when(mHelperBackend.getStarredContacts()).thenReturn(ImmutableList.of());
when(cursor.getCount()).thenReturn(1); when(mHelperBackend.getAllContacts()).thenReturn(
when(mHelperBackend.queryAllContactsData()).thenReturn(cursor); ImmutableList.of(new ZenHelperBackend.Contact(1, "The only contact", null)));
} when(mHelperBackend.getAllContactsCount()).thenReturn(1);
when(mHelperBackend.getImportantConversations()).thenReturn(ImmutableList.of());
// Makes a preference with the provided key and whether it's a checkbox with
// mSelectorClickListener as the onClickListener set.
private SelectorWithWidgetPreference makePreference(
String key, boolean isCheckbox, boolean isMessages) {
final SelectorWithWidgetPreference pref =
new SelectorWithWidgetPreference(mContext, isCheckbox);
pref.setKey(key);
pref.setOnClickListener(
isMessages ? mMessagesController.mSelectorClickListener
: mCallsController.mSelectorClickListener);
return pref;
} }
@Test @Test

View File

@@ -89,31 +89,38 @@ public class ZenModesSummaryHelperTest {
@Test @Test
public void getPeopleSummary_noOne() { public void getPeopleSummary_noOne() {
ZenMode zenMode = new TestModeBuilder() ZenPolicy policy = new ZenPolicy.Builder().disallowAllSounds().build();
.setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build())
.build();
assertThat(mSummaryHelper.getPeopleSummary(zenMode)).isEqualTo("No one can interrupt"); assertThat(mSummaryHelper.getPeopleSummary(policy)).isEqualTo("No one can interrupt");
} }
@Test @Test
public void getPeopleSummary_some() { public void getPeopleSummary_some() {
ZenMode zenMode = new TestModeBuilder() ZenPolicy policy = new ZenPolicy.Builder().allowCalls(PEOPLE_TYPE_CONTACTS).build();
.setZenPolicy(new ZenPolicy.Builder().allowCalls(PEOPLE_TYPE_CONTACTS).build())
assertThat(mSummaryHelper.getPeopleSummary(policy)).isEqualTo("Some people can interrupt");
}
@Test
public void getPeopleSummary_onlyRepeatCallers() {
ZenPolicy policy = new ZenPolicy.Builder()
.disallowAllSounds()
.allowRepeatCallers(true)
.build(); .build();
assertThat(mSummaryHelper.getPeopleSummary(zenMode)).isEqualTo("Some people can interrupt"); assertThat(mSummaryHelper.getPeopleSummary(policy)).isEqualTo(
"Repeat callers can interrupt");
} }
@Test @Test
public void getPeopleSummary_all() { public void getPeopleSummary_all() {
ZenMode zenMode = new TestModeBuilder() ZenPolicy policy = new ZenPolicy.Builder()
.setZenPolicy(new ZenPolicy.Builder().allowCalls(PEOPLE_TYPE_ANYONE). .allowCalls(PEOPLE_TYPE_ANYONE)
allowConversations(CONVERSATION_SENDERS_ANYONE) .allowConversations(CONVERSATION_SENDERS_ANYONE)
.allowMessages(PEOPLE_TYPE_ANYONE).build()) .allowMessages(PEOPLE_TYPE_ANYONE)
.build(); .build();
assertThat(mSummaryHelper.getPeopleSummary(zenMode)).isEqualTo("All people can interrupt"); assertThat(mSummaryHelper.getPeopleSummary(policy)).isEqualTo("All people can interrupt");
} }
@Test @Test