Merge changes Iceadf4a1,If2b6b06b into main

* changes:
  Final (?) touches to people/apps/sound circles
  Show icons for allowed contacts
This commit is contained in:
Matías Hernández
2024-07-22 17:27:01 +00:00
committed by Android (Google) Code Review
23 changed files with 821 additions and 123 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

@@ -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,880Q405,880 339.5,851.5Q274,823 225.5,774.5Q177,726 148.5,660.5Q120,595 120,520Q120,445 148.5,379.5Q177,314 225.5,265.5Q274,217 339.5,188.5Q405,160 480,160Q555,160 620.5,188.5Q686,217 734.5,265.5Q783,314 811.5,379.5Q840,445 840,520Q840,595 811.5,660.5Q783,726 734.5,774.5Q686,823 620.5,851.5Q555,880 480,880ZM480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520ZM592,688L648,632L520,504L520,320L440,320L440,536L592,688ZM224,94L280,150L110,320L54,264L224,94ZM736,94L906,264L850,320L680,150L736,94ZM480,800Q597,800 678.5,718.5Q760,637 760,520Q760,403 678.5,321.5Q597,240 480,240Q363,240 281.5,321.5Q200,403 200,520Q200,637 281.5,718.5Q363,800 480,800Z" />
</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="M580,720Q538,720 509,691Q480,662 480,620Q480,578 509,549Q538,520 580,520Q622,520 651,549Q680,578 680,620Q680,662 651,691Q622,720 580,720ZM200,880Q167,880 143.5,856.5Q120,833 120,800L120,240Q120,207 143.5,183.5Q167,160 200,160L240,160L240,80L320,80L320,160L640,160L640,80L720,80L720,160L760,160Q793,160 816.5,183.5Q840,207 840,240L840,800Q840,833 816.5,856.5Q793,880 760,880L200,880ZM200,800L760,800Q760,800 760,800Q760,800 760,800L760,400L200,400L200,800Q200,800 200,800Q200,800 200,800ZM200,320L760,320L760,240Q760,240 760,240Q760,240 760,240L200,240Q200,240 200,240Q200,240 200,240L200,320ZM200,320L200,240Q200,240 200,240Q200,240 200,240L200,240Q200,240 200,240Q200,240 200,240L200,320Z" />
</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="M400,840Q334,840 287,793Q240,746 240,680Q240,614 287,567Q334,520 400,520Q423,520 442.5,525.5Q462,531 480,542L480,120L720,120L720,280L560,280L560,680Q560,746 513,793Q466,840 400,840Z" />
</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="M260,320L300,320L300,280Q300,263 288.5,251.5Q277,240 260,240Q243,240 231.5,251.5Q220,263 220,280Q220,297 231.5,308.5Q243,320 260,320ZM440,320Q457,320 468.5,308.5Q480,297 480,280Q480,263 468.5,251.5Q457,240 440,240Q423,240 411.5,251.5Q400,263 400,280L400,320L440,320ZM502,620L502,620L502,620L502,620Q502,620 502,620Q502,620 502,620L502,620Q502,620 502,620Q502,620 502,620L502,620Q502,620 502,620Q502,620 502,620L502,620L502,620ZM419,880Q391,880 366.5,868Q342,856 325,834L107,557L126,537Q146,516 174,512Q202,508 226,523L300,568L300,400L260,400Q210,400 175,365Q140,330 140,280Q140,230 175,195Q210,160 260,160Q271,160 280.5,162Q290,164 300,167L300,120Q300,103 311.5,91.5Q323,80 340,80Q357,80 369,91.5Q381,103 381,120L381,176Q395,168 409.5,164Q424,160 440,160Q490,160 525,195Q560,230 560,280Q560,330 525,365Q490,400 440,400L381,400L381,712L284,652L388,785Q394,792 402,796Q410,800 419,800L640,800Q673,800 696.5,776.5Q720,753 720,720L720,560Q720,543 708.5,531.5Q697,520 680,520L461,520L461,440L680,440Q730,440 765,475Q800,510 800,560L800,720Q800,786 753,833Q706,880 640,880L419,880Z" />
</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="M419,880Q391,880 366.5,868Q342,856 325,834L107,557L126,537Q146,516 174,512Q202,508 226,523L300,568L300,240Q300,223 311.5,211.5Q323,200 340,200Q357,200 369,211.5Q381,223 381,240L381,712L284,652L388,785Q394,792 402,796Q410,800 419,800L640,800Q673,800 696.5,776.5Q720,753 720,720L720,560Q720,543 708.5,531.5Q697,520 680,520L461,520L461,440L680,440Q730,440 765,475Q800,510 800,560L800,720Q800,786 753,833Q706,880 640,880L419,880ZM167,340Q154,318 147,292.5Q140,267 140,240Q140,157 198.5,98.5Q257,40 340,40Q423,40 481.5,98.5Q540,157 540,240Q540,267 533,292.5Q526,318 513,340L444,300Q452,286 456,271.5Q460,257 460,240Q460,190 425,155Q390,120 340,120Q290,120 255,155Q220,190 220,240Q220,257 224,271.5Q228,286 236,300L167,340ZM502,620L502,620L502,620L502,620Q502,620 502,620Q502,620 502,620L502,620Q502,620 502,620Q502,620 502,620L502,620Q502,620 502,620Q502,620 502,620L502,620L502,620Z" />
</vector>

View File

@@ -22,7 +22,4 @@
android:width="@dimen/zen_mode_circular_icon_diameter"
android:height="@dimen/zen_mode_circular_icon_diameter" />
<solid android:color="?androidprv:attr/materialColorSecondaryContainer" />
<!-- TODO: b/346551087 - Include border (or not) according to final design
<stroke android:width="1dp" android:color="?androidprv:attr/materialColorOnSecondaryContainer" />
-->
</shape>

View File

@@ -513,4 +513,5 @@
<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_vertical">8dp</dimen>
<dimen name="zen_mode_circular_icon_text_size">18dp</dimen>
</resources>

View File

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

View File

@@ -20,8 +20,10 @@ import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.common.base.Equivalence;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ListenableFuture;
@@ -29,6 +31,7 @@ import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -67,8 +70,25 @@ class CircularIconSet<T> {
return MoreObjects.toStringHelper(this).add("items", mItems).toString();
}
boolean hasSameItemsAs(CircularIconSet<?> other) {
return other != null && this.mItems.equals(other.mItems);
@SuppressWarnings("unchecked")
<OtherT> boolean hasSameItemsAs(CircularIconSet<OtherT> other,
@Nullable Equivalence<OtherT> equivalence) {
if (other == null) {
return false;
}
if (other == this) {
return true;
}
if (equivalence == null) {
return mItems.equals(other.mItems);
}
// Check that types match before applying equivalence (statically unsafe). :(
Optional<Class<?>> thisItemClass = this.mItems.stream().findFirst().map(T::getClass);
Optional<Class<?>> otherItemClass = other.mItems.stream().findFirst().map(OtherT::getClass);
if (!thisItemClass.equals(otherItemClass)) {
return false;
}
return equivalence.pairwise().equivalent((Iterable<OtherT>) this.mItems, other.mItems);
}
int size() {

View File

@@ -41,6 +41,7 @@ import com.android.settings.R;
import com.android.settingslib.RestrictedPreference;
import com.android.settingslib.Utils;
import com.google.common.base.Equivalence;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@@ -105,8 +106,12 @@ public class CircularIconsPreference extends RestrictedPreference {
}
}
void displayIcons(CircularIconSet<?> iconSet) {
if (mIconSet != null && mIconSet.hasSameItemsAs(iconSet)) {
<T> void displayIcons(CircularIconSet<T> iconSet) {
displayIcons(iconSet, null);
}
<T> void displayIcons(CircularIconSet<T> iconSet, @Nullable Equivalence<T> itemEquivalence) {
if (mIconSet != null && mIconSet.hasSameItemsAs(iconSet, itemEquivalence)) {
return;
}
mIconSet = iconSet;
@@ -189,7 +194,6 @@ public class CircularIconsPreference extends RestrictedPreference {
}
// ... plus 0/1 TextViews at the end.
if (extraItems > 0 && !(getLastChild(mIconContainer) instanceof TextView)) {
// TODO: b/346551087 - Check TODO in preference_circular_icons_plus_item_background
TextView plusView = (TextView) inflater.inflate(
R.layout.preference_circular_icons_plus_item, mIconContainer, false);
mIconContainer.addView(plusView);

View File

@@ -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());

View File

@@ -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<ConversationChannelWrapper> getConversations(boolean onlyImportant) {
ImmutableList<ConversationChannelWrapper> getImportantConversations() {
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) {
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()) {
return getStarredContacts(cursor);
return getContactsFromCursor(cursor);
}
}
@VisibleForTesting
List<String> getStarredContacts(Cursor cursor) {
List<String> starredContacts = new ArrayList<>();
private ImmutableList<Contact> getContactsFromCursor(Cursor cursor) {
ImmutableList.Builder<Contact> 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);
}
}

View File

@@ -40,6 +40,7 @@ import com.android.settingslib.applications.ApplicationsState.AppEntry;
import com.android.settingslib.notification.modes.ZenMode;
import com.android.settingslib.notification.modes.ZenModesBackend;
import com.google.common.base.Equivalence;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;
@@ -47,6 +48,7 @@ import com.google.common.collect.Multimap;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
/**
* Preference with a link and summary about what apps can break through the mode
@@ -137,7 +139,8 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr
mPreference.setSummary(mSummaryHelper.getAppsSummary(mZenMode, apps));
mPreference.displayIcons(new CircularIconSet<>(apps,
app -> Utils.getBadgedIcon(mContext, app.info)));
app -> Utils.getBadgedIcon(mContext, app.info)),
APP_ENTRY_EQUIVALENCE);
}
@VisibleForTesting
@@ -158,6 +161,19 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr
.toList());
}
private static final Equivalence<AppEntry> APP_ENTRY_EQUIVALENCE = new Equivalence<>() {
@Override
protected boolean doEquivalent(@NonNull AppEntry a, @NonNull AppEntry b) {
return a.info.uid == b.info.uid
&& Objects.equals(a.info.packageName, b.info.packageName);
}
@Override
protected int doHash(@NonNull AppEntry entry) {
return Objects.hash(entry.info.uid, entry.info.packageName);
}
};
@VisibleForTesting
final ApplicationsState.Callbacks mAppSessionCallbacks =
new ApplicationsState.Callbacks() {

View File

@@ -29,6 +29,7 @@ import android.service.notification.ZenPolicy;
import androidx.annotation.NonNull;
import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settingslib.notification.modes.ZenMode;
import com.google.common.collect.ImmutableList;
@@ -41,19 +42,13 @@ import java.util.Map;
*/
class ZenModeOtherLinkPreferenceController extends AbstractZenModePreferenceController {
// TODO: b/346551087 - Use proper icons
private static final ImmutableMap</* @PriorityCategory */ Integer, /* @DrawableRes */ Integer>
PRIORITIES_TO_ICONS = ImmutableMap.of(
PRIORITY_CATEGORY_ALARMS,
com.android.internal.R.drawable.ic_audio_alarm,
PRIORITY_CATEGORY_MEDIA,
com.android.settings.R.drawable.ic_media_stream,
PRIORITY_CATEGORY_SYSTEM,
com.android.settings.R.drawable.ic_settings_keyboards,
PRIORITY_CATEGORY_REMINDERS,
com.android.internal.R.drawable.ic_popup_reminder,
PRIORITY_CATEGORY_EVENTS,
com.android.internal.R.drawable.ic_zen_mode_type_schedule_calendar);
PRIORITY_CATEGORY_ALARMS, R.drawable.ic_zen_mode_sound_alarms,
PRIORITY_CATEGORY_MEDIA, R.drawable.ic_zen_mode_sound_media,
PRIORITY_CATEGORY_SYSTEM, R.drawable.ic_zen_mode_sound_system,
PRIORITY_CATEGORY_REMINDERS, R.drawable.ic_zen_mode_sound_reminders,
PRIORITY_CATEGORY_EVENTS, R.drawable.ic_zen_mode_sound_events);
private final ZenModeSummaryHelper mSummaryHelper;
@@ -87,6 +82,6 @@ class ZenModeOtherLinkPreferenceController extends AbstractZenModePreferenceCont
}
}
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;
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<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.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<ConversationChannelWrapper> 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) {

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_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<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();
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<String, Object> 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);
}

View File

@@ -27,6 +27,7 @@ import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import com.google.common.base.Equivalence;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
@@ -54,33 +55,111 @@ public class CircularIconSetTest {
}
@Test
public void equals_sameItems_true() {
public void hasSameItemsAs_sameItems_true() {
CircularIconSet<Integer> items1 = new CircularIconSet<>(ImmutableList.of(1, 2),
num -> new ColorDrawable(Color.BLUE));
CircularIconSet<Integer> items2 = new CircularIconSet<>(ImmutableList.of(1, 2),
num -> new ColorDrawable(Color.GREEN));
assertThat(items1.hasSameItemsAs(items2)).isTrue();
assertThat(items1.hasSameItemsAs(items2, null)).isTrue();
}
@Test
public void equals_differentTypes_false() {
public void hasSameItemsAs_differentTypes_false() {
CircularIconSet<Integer> items1 = new CircularIconSet<>(ImmutableList.of(1, 2),
num -> new ColorDrawable(Color.BLUE));
CircularIconSet<String> items2 = new CircularIconSet<>(ImmutableList.of("a", "b"),
str -> new ColorDrawable(Color.GREEN));
assertThat(items1.hasSameItemsAs(items2)).isFalse();
assertThat(items1.hasSameItemsAs(items2, null)).isFalse();
}
@Test
public void equals_differentItems_false() {
public void hasSameItemsAs_differentItems_false() {
CircularIconSet<String> items1 = new CircularIconSet<>(ImmutableList.of("a", "b"),
str -> new ColorDrawable(Color.GREEN));
CircularIconSet<String> items2 = new CircularIconSet<>(ImmutableList.of("a", "b", "c"),
str -> new ColorDrawable(Color.GREEN));
assertThat(items1.hasSameItemsAs(items2)).isFalse();
assertThat(items1.hasSameItemsAs(items2, null)).isFalse();
}
private static class WrapperWithoutEquals<T> {
private final T mValue;
private WrapperWithoutEquals(T value) {
mValue = value;
}
}
@Test
public void hasSameItemsAs_withEquivalence_trueIfEquivalentItems() {
CircularIconSet<WrapperWithoutEquals<Integer>> items1 = new CircularIconSet<>(
ImmutableList.of(
new WrapperWithoutEquals<>(1),
new WrapperWithoutEquals<>(2)),
unused -> new ColorDrawable(Color.BLACK));
CircularIconSet<WrapperWithoutEquals<Integer>> items2 = new CircularIconSet<>(
ImmutableList.of(
new WrapperWithoutEquals<>(1),
new WrapperWithoutEquals<>(2)),
unused -> new ColorDrawable(Color.BLACK));
CircularIconSet<WrapperWithoutEquals<Integer>> items3 = new CircularIconSet<>(
ImmutableList.of(
new WrapperWithoutEquals<>(2),
new WrapperWithoutEquals<>(3)),
unused -> new ColorDrawable(Color.BLACK));
// Needs special equivalence, equals is not enough.
assertThat(items1.hasSameItemsAs(items2, null)).isFalse();
Equivalence<WrapperWithoutEquals<Integer>> equivalence = new Equivalence<>() {
@Override
protected boolean doEquivalent(WrapperWithoutEquals<Integer> a,
WrapperWithoutEquals<Integer> b) {
return a.mValue.equals(b.mValue);
}
@Override
protected int doHash(WrapperWithoutEquals<Integer> t) {
return t.mValue;
}
};
assertThat(items1.hasSameItemsAs(items2, equivalence)).isTrue();
assertThat(items1.hasSameItemsAs(items3, equivalence)).isFalse();
}
@Test
public void hasSameItemsAs_withEquivalenceButDifferentTypes_falseAndNoClassCastExceptions() {
CircularIconSet<Integer> items1 = new CircularIconSet<>(ImmutableList.of(1, 2),
num -> new ColorDrawable(Color.BLUE));
CircularIconSet<String> items2 = new CircularIconSet<>(ImmutableList.of("one", "two"),
num -> new ColorDrawable(Color.GREEN));
Equivalence<String> stringEquivalence = new Equivalence<String>() {
@Override
protected boolean doEquivalent(String a, String b) {
return a.equals(b);
}
@Override
protected int doHash(String t) {
return t.hashCode();
}
};
Equivalence<Integer> integerEquivalence = new Equivalence<Integer>() {
@Override
protected boolean doEquivalent(Integer a, Integer b) {
return a.equals(b);
}
@Override
protected int doHash(Integer t) {
return t.hashCode();
}
};
assertThat(items1.hasSameItemsAs(items2, stringEquivalence)).isFalse();
assertThat(items2.hasSameItemsAs(items1, integerEquivalence)).isFalse();
}
@Test

View File

@@ -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<Drawable>) 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<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 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

View File

@@ -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