diff --git a/res/drawable/ic_zen_mode_generic_contact.xml b/res/drawable/ic_zen_mode_generic_contact.xml new file mode 100644 index 00000000000..3721dc5ffd4 --- /dev/null +++ b/res/drawable/ic_zen_mode_generic_contact.xml @@ -0,0 +1,25 @@ + + + + \ No newline at end of file diff --git a/res/drawable/ic_zen_mode_people_all.xml b/res/drawable/ic_zen_mode_people_all.xml new file mode 100644 index 00000000000..c6194d52911 --- /dev/null +++ b/res/drawable/ic_zen_mode_people_all.xml @@ -0,0 +1,25 @@ + + + + \ No newline at end of file diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 2bb8fc2ba0f..68eb99b18da 100755 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -513,4 +513,5 @@ 20dp 4dp 8dp + 18dp diff --git a/res/values/strings.xml b/res/values/strings.xml index d5259a6a450..f994fbc911d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -9424,6 +9424,8 @@ Some people can interrupt + Repeat callers can interrupt + All people can interrupt diff --git a/src/com/android/settings/notification/modes/IconUtil.java b/src/com/android/settings/notification/modes/IconUtil.java index 07e14407353..e19da406d69 100644 --- a/src/com/android/settings/notification/modes/IconUtil.java +++ b/src/com/android/settings/notification/modes/IconUtil.java @@ -20,6 +20,13 @@ import static com.google.common.base.Preconditions.checkNotNull; import android.content.Context; 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.LayerDrawable; import android.graphics.drawable.ShapeDrawable; @@ -29,11 +36,16 @@ import android.view.Gravity; import androidx.annotation.AttrRes; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.Px; import com.android.settings.R; import com.android.settingslib.Utils; +import com.google.common.base.Strings; + +import java.util.Locale; + class IconUtil { 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 * 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( Utils.getColorAttr(context, com.android.internal.R.attr.materialColorSecondaryContainer), @@ -102,6 +115,53 @@ class IconUtil { 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, Drawable icon, ColorStateList iconColor, @Px int iconSizePx) { ShapeDrawable background = new ShapeDrawable(new OvalShape()); diff --git a/src/com/android/settings/notification/modes/ZenHelperBackend.java b/src/com/android/settings/notification/modes/ZenHelperBackend.java index 4136c2210bc..a2c3578af07 100644 --- a/src/com/android/settings/notification/modes/ZenHelperBackend.java +++ b/src/com/android/settings/notification/modes/ZenHelperBackend.java @@ -21,15 +21,22 @@ import android.app.INotificationManager; import android.content.Context; import android.content.pm.ParceledListSlice; import android.database.Cursor; +import android.graphics.drawable.Drawable; +import android.net.Uri; import android.os.ServiceManager; import android.provider.ContactsContract; import android.service.notification.ConversationChannelWrapper; 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.List; @@ -75,45 +82,99 @@ class ZenHelperBackend { } @SuppressWarnings("unchecked") - ParceledListSlice getConversations(boolean onlyImportant) { + ImmutableList getImportantConversations() { try { - return mInm.getConversations(onlyImportant); + ImmutableList.Builder list = new ImmutableList.Builder<>(); + ParceledListSlice 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) { Log.w(TAG, "Error calling NoMan", e); - return ParceledListSlice.emptyList(); + return ImmutableList.of(); } } - List getStarredContacts() { + record Contact(long id, @Nullable String displayName, @Nullable Uri photoUri) { } + + ImmutableList getAllContacts() { + try (Cursor cursor = queryAllContactsData()) { + return getContactsFromCursor(cursor); + } + } + + ImmutableList getStarredContacts() { try (Cursor cursor = queryStarredContactsData()) { - return getStarredContacts(cursor); + return getContactsFromCursor(cursor); } } - @VisibleForTesting - List getStarredContacts(Cursor cursor) { - List starredContacts = new ArrayList<>(); + private ImmutableList getContactsFromCursor(Cursor cursor) { + ImmutableList.Builder list = new ImmutableList.Builder<>(); if (cursor != null && cursor.moveToFirst()) { do { - String contact = cursor.getString(0); - starredContacts.add(contact != null ? contact : - mContext.getString(R.string.zen_mode_starred_contacts_empty_name)); - + long id = cursor.getLong(0); + String name = Strings.emptyToNull(cursor.getString(1)); + String photoUriStr = cursor.getString(2); + Uri photoUri = !Strings.isNullOrEmpty(photoUriStr) ? Uri.parse(photoUriStr) : null; + list.add(new Contact(id, name, photoUri)); } 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() { - return mContext.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, - new String[]{ContactsContract.Contacts.DISPLAY_NAME_PRIMARY}, - ContactsContract.Data.STARRED + "=1", null, - ContactsContract.Data.TIMES_CONTACTED); + return mContext.getContentResolver().query( + ContactsContract.Contacts.CONTENT_URI, + CONTACTS_PROJECTION, + /* selection= */ ContactsContract.Data.STARRED + "=1", /* selectionArgs= */ null, + /* sortOrder= */ ContactsContract.Contacts.DISPLAY_NAME_PRIMARY); } - Cursor queryAllContactsData() { - return mContext.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, - new String[]{ContactsContract.Contacts.DISPLAY_NAME_PRIMARY}, - null, null, null); + private Cursor queryAllContactsData() { + return mContext.getContentResolver().query( + ContactsContract.Contacts.CONTENT_URI, + 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); } } diff --git a/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java index fce48afe443..206ad42bc7b 100644 --- a/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java @@ -87,6 +87,6 @@ class ZenModeOtherLinkPreferenceController extends AbstractZenModePreferenceCont } } return new CircularIconSet<>(icons.build(), - iconResId -> IconUtil.makeSoundIcon(mContext, iconResId)); + iconResId -> IconUtil.makeCircularIconPreferenceItem(mContext, iconResId)); } } diff --git a/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java index 2a614188801..762cdd57ca6 100644 --- a/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java @@ -17,25 +17,67 @@ 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.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.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 { 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) { + 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 @@ -50,8 +92,104 @@ class ZenModePeopleLinkPreferenceController extends AbstractZenModePreferenceCon 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); + preference.setSummary(mSummaryHelper.getPeopleSummary(zenMode.getPolicy())); + ((CircularIconsPreference) preference).displayIcons(getPeopleIcons(zenMode.getPolicy())); + } + + // Represents "Either". + 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 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 peopleItems) { + @PeopleType int peopleAllowed = getCallersOrMessagesAllowed(policy); + + ImmutableList 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 peopleItems) { + @ConversationSenders int conversationSendersAllowed = + policy.getPriorityCategoryConversations() == STATE_ALLOW + ? policy.getPriorityConversationSenders() + : CONVERSATION_SENDERS_NONE; + ImmutableList 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!"); + } } } diff --git a/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java b/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java index 0f9323d81fc..32c6a9881f0 100644 --- a/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java @@ -30,10 +30,8 @@ import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; -import android.content.pm.ParceledListSlice; import android.icu.text.MessageFormat; import android.provider.Contacts; -import android.service.notification.ConversationChannelWrapper; import android.service.notification.ZenPolicy; import android.view.View; @@ -167,17 +165,7 @@ class ZenModePrioritySendersPreferenceController } private void updateChannelCounts() { - ParceledListSlice impConversations = - mHelperBackend.getConversations(true); - int numImportantConversations = 0; - if (impConversations != null) { - for (ConversationChannelWrapper conversation : impConversations.getList()) { - if (!conversation.getNotificationChannel().isDemoted()) { - numImportantConversations++; - } - } - } - mNumImportantConversations = numImportantConversations; + mNumImportantConversations = mHelperBackend.getImportantConversations().size(); } private int getPrioritySenders(ZenPolicy policy) { diff --git a/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java b/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java index dd3a400e886..1acef20ac6c 100644 --- a/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java +++ b/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java @@ -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_REPEAT_CALLERS; 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_BADGE; 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.ZenModeConfig; import android.service.notification.ZenPolicy; +import android.service.notification.ZenPolicy.ConversationSenders; +import android.service.notification.ZenPolicy.PeopleType; import android.util.ArrayMap; import androidx.annotation.NonNull; @@ -56,6 +59,7 @@ import com.android.settings.R; import com.android.settingslib.applications.ApplicationsState.AppEntry; import com.android.settingslib.notification.modes.ZenMode; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import java.util.ArrayList; @@ -365,7 +369,12 @@ class ZenModeSummaryHelper { } public String getStarredContactsSummary() { - List starredContacts = mBackend.getStarredContacts(); + List 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(); MessageFormat msgFormat = new MessageFormat( mContext.getString(R.string.zen_mode_starred_contacts_summary_contacts), @@ -389,26 +398,32 @@ class ZenModeSummaryHelper { mContext.getString(R.string.zen_mode_contacts_count), Locale.getDefault()); Map args = new HashMap<>(); - args.put("count", mBackend.queryAllContactsData().getCount()); + args.put("count", mBackend.getAllContactsCount()); return msgFormat.format(args); } - public String getPeopleSummary(ZenMode zenMode) { - final int callersAllowed = zenMode.getPolicy().getPriorityCallSenders(); - final int messagesAllowed = zenMode.getPolicy().getPriorityMessageSenders(); - final int conversationsAllowed = zenMode.getPolicy().getPriorityConversationSenders(); + public String getPeopleSummary(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; + @ConversationSenders int conversationsAllowed = + policy.getPriorityCategoryConversations() == STATE_ALLOW + ? policy.getPriorityConversationSenders() + : CONVERSATION_SENDERS_NONE; final boolean areRepeatCallersAllowed = - zenMode.getPolicy().isCategoryAllowed(PRIORITY_CATEGORY_REPEAT_CALLERS, false); + policy.isCategoryAllowed(PRIORITY_CATEGORY_REPEAT_CALLERS, false); if (callersAllowed == PEOPLE_TYPE_ANYONE && messagesAllowed == PEOPLE_TYPE_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 && messagesAllowed == PEOPLE_TYPE_NONE - && conversationsAllowed == CONVERSATION_SENDERS_NONE - && !areRepeatCallersAllowed) { - return mContext.getResources().getString(R.string.zen_mode_people_none); + && conversationsAllowed == CONVERSATION_SENDERS_NONE) { + return mContext.getString( + areRepeatCallersAllowed ? R.string.zen_mode_people_repeat_callers + : R.string.zen_mode_people_none); } else { return mContext.getResources().getString(R.string.zen_mode_people_some); } diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java index 0db26c3cae4..8ec980d30aa 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java @@ -16,17 +16,47 @@ 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.eq; -import static org.mockito.Mockito.mock; +import static org.mockito.ArgumentMatchers.anyBoolean; +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.when; import android.app.Flags; +import android.app.NotificationChannel; +import android.app.NotificationManager; 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.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.ZenMode; + +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.MoreExecutors; import org.junit.Before; import org.junit.Rule; @@ -34,39 +64,160 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import java.util.Collection; + +@EnableFlags(Flags.FLAG_MODES_UI) @RunWith(RobolectricTestRunner.class) public final class ZenModePeopleLinkPreferenceControllerTest { private ZenModePeopleLinkPreferenceController mController; + private CircularIconsPreference mPreference; @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private Context mContext; - @Mock - private ZenHelperBackend mHelperBackend; + @Mock private ZenHelperBackend mHelperBackend; + @Mock private ConversationIconFactory mConversationIconFactory; @Before public void setup() { MockitoAnnotations.initMocks(this); - 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( - mContext, "something", mHelperBackend); + mContext, "something", mHelperBackend, mConversationIconFactory); + + setUpContacts(ImmutableList.of(), ImmutableList.of()); + setUpImportantConversations(ImmutableList.of()); + + when(mHelperBackend.getContactPhoto(any())).then( + (Answer) invocationOnMock -> photoOf(invocationOnMock.getArgument(0))); + when(mConversationIconFactory.getConversationDrawable((ShortcutInfo) any(), any(), anyInt(), + anyBoolean())).thenReturn(new ColorDrawable(Color.BLACK)); } @Test - @EnableFlags(Flags.FLAG_MODES_UI) - public void testHasSummary() { - CircularIconsPreference pref = mock(CircularIconsPreference.class); + public void updateState_setsSummary() { + mController.updateState(mPreference, TestModeBuilder.EXAMPLE); - mController.updateZenMode(pref, TestModeBuilder.EXAMPLE); + assertThat(mPreference.getSummary()).isNotNull(); + assertThat(mPreference.getSummary().toString()).isNotEmpty(); + } - verify(pref).setSummary(any()); - verify(pref).displayIcons(eq(CircularIconSet.EMPTY)); + @Test + 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 allIds, Collection 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 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()); } } \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceControllerTest.java index 944d4325644..64de1418ad6 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceControllerTest.java @@ -35,13 +35,11 @@ import static com.android.settings.notification.modes.ZenModePrioritySendersPref 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.when; import android.app.Flags; import android.content.Context; -import android.database.Cursor; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; 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.widget.SelectorWithWidgetPreference; +import com.google.common.collect.ImmutableList; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -105,22 +105,11 @@ public final class ZenModePrioritySendersPreferenceControllerTest { mPreferenceScreen.addPreference(mCallsPrefCategory); mPreferenceScreen.addPreference(mMessagesPrefCategory); - Cursor cursor = mock(Cursor.class); - when(cursor.getCount()).thenReturn(1); - when(mHelperBackend.queryAllContactsData()).thenReturn(cursor); - } - - // 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; + when(mHelperBackend.getStarredContacts()).thenReturn(ImmutableList.of()); + when(mHelperBackend.getAllContacts()).thenReturn( + ImmutableList.of(new ZenHelperBackend.Contact(1, "The only contact", null))); + when(mHelperBackend.getAllContactsCount()).thenReturn(1); + when(mHelperBackend.getImportantConversations()).thenReturn(ImmutableList.of()); } @Test diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java index 672a0d74355..a7257f53705 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java @@ -89,31 +89,38 @@ public class ZenModesSummaryHelperTest { @Test public void getPeopleSummary_noOne() { - ZenMode zenMode = new TestModeBuilder() - .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build()) - .build(); + ZenPolicy policy = new ZenPolicy.Builder().disallowAllSounds().build(); - assertThat(mSummaryHelper.getPeopleSummary(zenMode)).isEqualTo("No one can interrupt"); + assertThat(mSummaryHelper.getPeopleSummary(policy)).isEqualTo("No one can interrupt"); } @Test public void getPeopleSummary_some() { - ZenMode zenMode = new TestModeBuilder() - .setZenPolicy(new ZenPolicy.Builder().allowCalls(PEOPLE_TYPE_CONTACTS).build()) + ZenPolicy policy = 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(); - assertThat(mSummaryHelper.getPeopleSummary(zenMode)).isEqualTo("Some people can interrupt"); + assertThat(mSummaryHelper.getPeopleSummary(policy)).isEqualTo( + "Repeat callers can interrupt"); } @Test public void getPeopleSummary_all() { - ZenMode zenMode = new TestModeBuilder() - .setZenPolicy(new ZenPolicy.Builder().allowCalls(PEOPLE_TYPE_ANYONE). - allowConversations(CONVERSATION_SENDERS_ANYONE) - .allowMessages(PEOPLE_TYPE_ANYONE).build()) + ZenPolicy policy = new ZenPolicy.Builder() + .allowCalls(PEOPLE_TYPE_ANYONE) + .allowConversations(CONVERSATION_SENDERS_ANYONE) + .allowMessages(PEOPLE_TYPE_ANYONE) .build(); - assertThat(mSummaryHelper.getPeopleSummary(zenMode)).isEqualTo("All people can interrupt"); + assertThat(mSummaryHelper.getPeopleSummary(policy)).isEqualTo("All people can interrupt"); } @Test