1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.service.notification;
18 
19 import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE;
20 import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL;
21 import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_POSITIVE;
22 
23 import static junit.framework.Assert.assertEquals;
24 import static junit.framework.Assert.assertFalse;
25 import static junit.framework.Assert.assertNotNull;
26 import static junit.framework.Assert.assertNull;
27 import static junit.framework.Assert.assertTrue;
28 
29 import static org.junit.Assert.assertArrayEquals;
30 import static org.junit.Assert.assertNotEquals;
31 import static org.mockito.Mockito.spy;
32 
33 import android.app.Notification;
34 import android.app.NotificationChannel;
35 import android.app.NotificationManager;
36 import android.app.PendingIntent;
37 import android.content.ComponentName;
38 import android.content.Intent;
39 import android.content.pm.ShortcutInfo;
40 import android.os.Bundle;
41 import android.os.Parcel;
42 import android.os.SharedMemory;
43 import android.platform.test.flag.junit.SetFlagsRule;
44 import android.testing.TestableContext;
45 
46 import androidx.test.InstrumentationRegistry;
47 import androidx.test.filters.SmallTest;
48 
49 import org.junit.Assert;
50 import org.junit.Before;
51 import org.junit.Rule;
52 import org.junit.Test;
53 import org.junit.runner.RunWith;
54 import org.junit.runners.Parameterized;
55 
56 import java.util.ArrayList;
57 import java.util.List;
58 
59 @SmallTest
60 @RunWith(Parameterized.class)
61 public class NotificationRankingUpdateTest {
62 
63     private static final String NOTIFICATION_CHANNEL_ID = "test_channel_id";
64     private static final String TEST_KEY = "key";
65 
66     private NotificationChannel mNotificationChannel;
67 
68     @Rule
69     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
70 
71     // TODO(b/284297289): remove this flag set once resolved.
72     @Parameterized.Parameters(name = "rankingUpdateAshmem={0}")
getRankingUpdateAshmem()73     public static Boolean[] getRankingUpdateAshmem() {
74         return new Boolean[] { true, false };
75     }
76 
77     @Parameterized.Parameter
78     public boolean mRankingUpdateAshmem;
79 
80     @Rule
81     public TestableContext mContext =
82             spy(new TestableContext(InstrumentationRegistry.getContext(), null));
83 
getContext()84     protected TestableContext getContext() {
85         return mContext;
86     }
87 
88     public static String[] mKeys = new String[] { "key", "key1", "key2", "key3", "key4"};
89 
90     /**
91      * Creates a NotificationRankingUpdate with prepopulated Ranking entries
92      * @param context A testable context, used for PendingIntent creation
93      * @return The NotificationRankingUpdate to be used as test data
94      */
generateUpdate(TestableContext context)95     public static NotificationRankingUpdate generateUpdate(TestableContext context) {
96         NotificationListenerService.Ranking[] rankings =
97                 new NotificationListenerService.Ranking[mKeys.length];
98         for (int i = 0; i < mKeys.length; i++) {
99             final String key = mKeys[i];
100             NotificationListenerService.Ranking ranking = new NotificationListenerService.Ranking();
101             ranking.populate(
102                     key,
103                     i,
104                     !isIntercepted(i),
105                     getVisibilityOverride(i),
106                     getSuppressedVisualEffects(i),
107                     getImportance(i),
108                     getExplanation(key),
109                     getOverrideGroupKey(key),
110                     getChannel(key, i),
111                     getPeople(key, i),
112                     getSnoozeCriteria(key, i),
113                     getShowBadge(i),
114                     getUserSentiment(i),
115                     getHidden(i),
116                     lastAudiblyAlerted(i),
117                     getNoisy(i),
118                     getSmartActions(key, i, context),
119                     getSmartReplies(key, i),
120                     canBubble(i),
121                     isTextChanged(i),
122                     isConversation(i),
123                     getShortcutInfo(i),
124                     getRankingAdjustment(i),
125                     isBubble(i),
126                     getProposedImportance(i),
127                     hasSensitiveContent(i)
128             );
129             rankings[i] = ranking;
130         }
131         return new NotificationRankingUpdate(rankings);
132     }
133 
134     /**
135      * Produces a visibility override value based on the provided index.
136      */
getVisibilityOverride(int index)137     public static int getVisibilityOverride(int index) {
138         return index * 9;
139     }
140 
141     /**
142      * Produces a group key based on the provided key.
143      */
getOverrideGroupKey(String key)144     public static String getOverrideGroupKey(String key) {
145         return key + key;
146     }
147 
148     /**
149      * Produces a boolean that can be used to represent isIntercepted, based on the provided index.
150      */
isIntercepted(int index)151     public static boolean isIntercepted(int index) {
152         return index % 2 == 0;
153     }
154 
155     /**
156      * Produces a suppressed visual effects value based on the provided index
157      */
getSuppressedVisualEffects(int index)158     public static int getSuppressedVisualEffects(int index) {
159         return index * 2;
160     }
161 
162     /**
163      * Produces an importance value, based on the provided index
164      */
getImportance(int index)165     public static int getImportance(int index) {
166         return index;
167     }
168 
169     /**
170      * Produces an explanation value, based on the provided key
171      */
getExplanation(String key)172     public static String getExplanation(String key) {
173         return key + "explain";
174     }
175 
176     /**
177      * Produces a notification channel, based on the provided key and index
178      */
getChannel(String key, int index)179     public static NotificationChannel getChannel(String key, int index) {
180         return new NotificationChannel(key, key, getImportance(index));
181     }
182 
183     /**
184      * Produces a boolean that can be used to represent showBadge, based on the provided index
185      */
getShowBadge(int index)186     public static boolean getShowBadge(int index) {
187         return index % 3 == 0;
188     }
189 
190     /**
191      * Produces a user sentiment value, based on the provided index
192      */
getUserSentiment(int index)193     public static int getUserSentiment(int index) {
194         switch(index % 3) {
195             case 0:
196                 return USER_SENTIMENT_NEGATIVE;
197             case 1:
198                 return USER_SENTIMENT_NEUTRAL;
199             case 2:
200                 return USER_SENTIMENT_POSITIVE;
201         }
202         return USER_SENTIMENT_NEUTRAL;
203     }
204 
205     /**
206      * Produces a boolean that can be used to represent "hidden," based on the provided index.
207      */
getHidden(int index)208     public static boolean getHidden(int index) {
209         return index % 2 == 0;
210     }
211 
212     /**
213      * Produces a long to represent lastAudiblyAlerted based on the provided index.
214      */
lastAudiblyAlerted(int index)215     public static long lastAudiblyAlerted(int index) {
216         return index * 2000L;
217     }
218 
219     /**
220      * Produces a boolean that can be used to represent "noisy," based on the provided index.
221      */
getNoisy(int index)222     public static boolean getNoisy(int index) {
223         return index < 1;
224     }
225 
226     /**
227      * Produces strings that can be used to represent people, based on the provided key and index.
228      */
getPeople(String key, int index)229     public static ArrayList<String> getPeople(String key, int index) {
230         ArrayList<String> people = new ArrayList<>();
231         for (int i = 0; i < index; i++) {
232             people.add(i + key);
233         }
234         return people;
235     }
236 
237     /**
238      * Produces a number of snoozeCriteria, based on the provided key and index.
239      */
getSnoozeCriteria(String key, int index)240     public static ArrayList<SnoozeCriterion> getSnoozeCriteria(String key, int index) {
241         ArrayList<SnoozeCriterion> snooze = new ArrayList<>();
242         for (int i = 0; i < index; i++) {
243             snooze.add(new SnoozeCriterion(key + i, getExplanation(key), key));
244         }
245         return snooze;
246     }
247 
248     /**
249      * Produces a list of Actions which can be used to represent smartActions.
250      * These actions are built from pending intents with intent titles based on the provided
251      * key, and ids based on the provided index.
252      */
getSmartActions(String key, int index, TestableContext context)253     public static ArrayList<Notification.Action> getSmartActions(String key,
254                                                                  int index,
255                                                                  TestableContext context) {
256         ArrayList<Notification.Action> actions = new ArrayList<>();
257         for (int i = 0; i < index; i++) {
258             PendingIntent intent = PendingIntent.getBroadcast(
259                     context,
260                     index /*requestCode*/,
261                     new Intent("ACTION_" + key),
262                     PendingIntent.FLAG_IMMUTABLE /*flags*/);
263             actions.add(new Notification.Action.Builder(null /*icon*/, key, intent).build());
264         }
265         return actions;
266     }
267 
268     /**
269      * Produces index number of "smart replies," all based on the provided key and index
270      */
getSmartReplies(String key, int index)271     public static ArrayList<CharSequence> getSmartReplies(String key, int index) {
272         ArrayList<CharSequence> choices = new ArrayList<>();
273         for (int i = 0; i < index; i++) {
274             choices.add("choice_" + key + "_" + i);
275         }
276         return choices;
277     }
278 
279     /**
280      * Produces a boolean that can be  used to represent canBubble, based on the provided index
281      */
canBubble(int index)282     public static boolean canBubble(int index) {
283         return index % 4 == 0;
284     }
285 
286     /**
287      * Produces a boolean that can be used to represent isTextChanged, based on the provided index.
288      */
isTextChanged(int index)289     public static boolean isTextChanged(int index) {
290         return index % 4 == 0;
291     }
292 
293     /**
294      * Produces a boolean that can be used to represent isConversation, based on the provided index.
295      */
isConversation(int index)296     public static boolean isConversation(int index) {
297         return index % 4 == 0;
298     }
299 
300     /**
301      * Produces a ShortcutInfo value based on the provided index.
302      */
getShortcutInfo(int index)303     public static ShortcutInfo getShortcutInfo(int index) {
304         ShortcutInfo si = new ShortcutInfo(
305                 index, String.valueOf(index), "packageName", new ComponentName("1", "1"), null,
306                 "title", 0, "titleResName", "text", 0, "textResName",
307                 "disabledMessage", 0, "disabledMessageResName",
308                 null, null, 0, null, 0, 0,
309                 0, "iconResName", "bitmapPath", null, 0,
310                 null, null, null, null);
311         return si;
312     }
313 
314     /**
315      * Produces a rankingAdjustment value, based on the provided index.
316      */
getRankingAdjustment(int index)317     public static int getRankingAdjustment(int index) {
318         return index % 3 - 1;
319     }
320 
321     /**
322      * Produces a proposedImportance, based on the provided index.
323      */
getProposedImportance(int index)324     public static int getProposedImportance(int index) {
325         return index % 5 - 1;
326     }
327 
328     /**
329      * Produces a boolean that can be used to represent hasSensitiveContent, based on the provided
330      * index.
331      */
hasSensitiveContent(int index)332     public static boolean hasSensitiveContent(int index) {
333         return index % 3 == 0;
334     }
335 
336     /**
337      * Produces a boolean that can be used to represent isBubble, based on the provided index.
338      */
isBubble(int index)339     public static boolean isBubble(int index) {
340         return index % 4 == 0;
341     }
342 
343     /**
344      * Checks that each of the pairs of actions in the two provided lists has identical titles,
345      * and that the lists have the same number of elements.
346      */
assertActionsEqual( List<Notification.Action> expecteds, List<Notification.Action> actuals)347     public void assertActionsEqual(
348             List<Notification.Action> expecteds, List<Notification.Action> actuals) {
349         Assert.assertEquals(expecteds.size(), actuals.size());
350         for (int i = 0; i < expecteds.size(); i++) {
351             Notification.Action expected = expecteds.get(i);
352             Notification.Action actual = actuals.get(i);
353             Assert.assertEquals(expected.title.toString(), actual.title.toString());
354         }
355     }
356 
357     /**
358      * Checks that all subelements of the provided NotificationRankingUpdates are equal.
359      */
detailedAssertEquals(NotificationRankingUpdate a, NotificationRankingUpdate b)360     public void detailedAssertEquals(NotificationRankingUpdate a, NotificationRankingUpdate b) {
361         detailedAssertEquals(a.getRankingMap(), b.getRankingMap());
362     }
363 
364     /**
365      * Checks that all subelements of the provided Ranking objects are equal.
366      */
detailedAssertEquals(String comment, NotificationListenerService.Ranking a, NotificationListenerService.Ranking b)367     public void detailedAssertEquals(String comment, NotificationListenerService.Ranking a,
368                                      NotificationListenerService.Ranking b) {
369         Assert.assertEquals(comment, a.getKey(), b.getKey());
370         Assert.assertEquals(comment, a.getRank(), b.getRank());
371         Assert.assertEquals(comment, a.matchesInterruptionFilter(), b.matchesInterruptionFilter());
372         Assert.assertEquals(comment, a.getLockscreenVisibilityOverride(),
373                 b.getLockscreenVisibilityOverride());
374         Assert.assertEquals(comment, a.getSuppressedVisualEffects(),
375                 b.getSuppressedVisualEffects());
376         Assert.assertEquals(comment, a.getImportance(), b.getImportance());
377         Assert.assertEquals(comment, a.getImportanceExplanation(), b.getImportanceExplanation());
378         Assert.assertEquals(comment, a.getOverrideGroupKey(), b.getOverrideGroupKey());
379         Assert.assertEquals(comment, a.getChannel().toString(), b.getChannel().toString());
380         Assert.assertEquals(comment, a.getAdditionalPeople(), b.getAdditionalPeople());
381         Assert.assertEquals(comment, a.getSnoozeCriteria(), b.getSnoozeCriteria());
382         Assert.assertEquals(comment, a.canShowBadge(), b.canShowBadge());
383         Assert.assertEquals(comment, a.getUserSentiment(), b.getUserSentiment());
384         Assert.assertEquals(comment, a.isSuspended(), b.isSuspended());
385         Assert.assertEquals(comment, a.getLastAudiblyAlertedMillis(),
386                 b.getLastAudiblyAlertedMillis());
387         Assert.assertEquals(comment, a.isNoisy(), b.isNoisy());
388         Assert.assertEquals(comment, a.getSmartReplies(), b.getSmartReplies());
389         Assert.assertEquals(comment, a.canBubble(), b.canBubble());
390         Assert.assertEquals(comment, a.isConversation(), b.isConversation());
391         if (a.getConversationShortcutInfo() != null && b.getConversationShortcutInfo() != null) {
392             Assert.assertEquals(comment, a.getConversationShortcutInfo().getId(),
393                     b.getConversationShortcutInfo().getId());
394         } else {
395             // One or both must be null, so we can check for equality.
396             Assert.assertEquals(a.getConversationShortcutInfo(), b.getConversationShortcutInfo());
397         }
398         assertActionsEqual(a.getSmartActions(), b.getSmartActions());
399         Assert.assertEquals(a.getProposedImportance(), b.getProposedImportance());
400         Assert.assertEquals(a.hasSensitiveContent(), b.hasSensitiveContent());
401     }
402 
403     /**
404      * Checks that the two RankingMaps have identical keys, and that each Ranking object for
405      * each of those keys is identical.
406      */
detailedAssertEquals(NotificationListenerService.RankingMap a, NotificationListenerService.RankingMap b)407     public void detailedAssertEquals(NotificationListenerService.RankingMap a,
408                                      NotificationListenerService.RankingMap b) {
409         NotificationListenerService.Ranking arank = new NotificationListenerService.Ranking();
410         NotificationListenerService.Ranking brank = new NotificationListenerService.Ranking();
411         assertArrayEquals(a.getOrderedKeys(), b.getOrderedKeys());
412         for (String key : a.getOrderedKeys()) {
413             a.getRanking(key, arank);
414             b.getRanking(key, brank);
415             detailedAssertEquals("ranking for key <" + key + ">", arank, brank);
416         }
417     }
418 
419     @Before
setUp()420     public void setUp() {
421         mNotificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, "test channel",
422                 NotificationManager.IMPORTANCE_DEFAULT);
423 
424         if (mRankingUpdateAshmem) {
425             mSetFlagsRule.enableFlags(Flags.FLAG_RANKING_UPDATE_ASHMEM);
426         } else {
427             mSetFlagsRule.disableFlags(Flags.FLAG_RANKING_UPDATE_ASHMEM);
428         }
429     }
430 
431     /**
432      * Creates a mostly empty Test Ranking object with the specified key, rank, and smartActions.
433      */
createEmptyTestRanking( String key, int rank, ArrayList<Notification.Action> actions)434     public NotificationListenerService.Ranking createEmptyTestRanking(
435             String key, int rank, ArrayList<Notification.Action> actions) {
436         NotificationListenerService.Ranking ranking = new NotificationListenerService.Ranking();
437 
438         ranking.populate(
439                 /* key= */ key,
440                 /* rank= */ rank,
441                 /* matchesInterruptionFilter= */ false,
442                 /* visibilityOverride= */ 0,
443                 /* suppressedVisualEffects= */ 0,
444                 mNotificationChannel.getImportance(),
445                 /* explanation= */ null,
446                 /* overrideGroupKey= */ null,
447                 mNotificationChannel,
448                 /* overridePeople= */ null,
449                 /* snoozeCriteria= */ null,
450                 /* showBadge= */ true,
451                 /* userSentiment= */ 0,
452                 /* hidden= */ false,
453                 /* lastAudiblyAlertedMs= */ -1,
454                 /* noisy= */ false,
455                 /* smartActions= */ actions,
456                 /* smartReplies= */ null,
457                 /* canBubble= */ false,
458                 /* isTextChanged= */ false,
459                 /* isConversation= */ false,
460                 /* shortcutInfo= */ null,
461                 /* rankingAdjustment= */ 0,
462                 /* isBubble= */ false,
463                 /* proposedImportance= */ 0,
464                 /* sensitiveContent= */ false
465         );
466         return ranking;
467     }
468 
469     // Tests parceling of NotificationRankingUpdate, and by extension, RankingMap and Ranking.
470     @Test
testRankingUpdate_parcel()471     public void testRankingUpdate_parcel() {
472         NotificationRankingUpdate nru = generateUpdate(getContext());
473         Parcel parcel = Parcel.obtain();
474         nru.writeToParcel(parcel, 0);
475         if (Flags.rankingUpdateAshmem()) {
476             assertTrue(nru.isFdNotNullAndClosed());
477         }
478         parcel.setDataPosition(0);
479         NotificationRankingUpdate nru1 = NotificationRankingUpdate.CREATOR.createFromParcel(parcel);
480         // The rankingUpdate file descriptor is only non-null in the new path.
481         if (Flags.rankingUpdateAshmem()) {
482             assertTrue(nru1.isFdNotNullAndClosed());
483         }
484         detailedAssertEquals(nru, nru1);
485         parcel.recycle();
486     }
487 
488     // Tests parceling of RankingMap and RankingMap.equals
489     @Test
testRankingMap_parcel()490     public void testRankingMap_parcel() {
491         NotificationListenerService.RankingMap rmap = generateUpdate(getContext()).getRankingMap();
492         Parcel parcel = Parcel.obtain();
493         rmap.writeToParcel(parcel, 0);
494         parcel.setDataPosition(0);
495         NotificationListenerService.RankingMap rmap1 =
496                 NotificationListenerService.RankingMap.CREATOR.createFromParcel(parcel);
497 
498         detailedAssertEquals(rmap, rmap1);
499         Assert.assertEquals(rmap, rmap1);
500         parcel.recycle();
501     }
502 
503     // Tests parceling of Ranking and Ranking.equals
504     @Test
testRanking_parcel()505     public void testRanking_parcel() {
506         NotificationListenerService.Ranking ranking =
507                 generateUpdate(getContext()).getRankingMap().getRawRankingObject(mKeys[0]);
508         Parcel parcel = Parcel.obtain();
509         ranking.writeToParcel(parcel, 0);
510         parcel.setDataPosition(0);
511         NotificationListenerService.Ranking ranking1 =
512                 new NotificationListenerService.Ranking(parcel);
513         detailedAssertEquals("rankings differ: ", ranking, ranking1);
514         Assert.assertEquals(ranking, ranking1);
515         parcel.recycle();
516     }
517 
518     // Tests NotificationRankingUpdate.equals(), and by extension, RankingMap and Ranking.
519     @Test
testRankingUpdate_equals_legacy()520     public void testRankingUpdate_equals_legacy() {
521         NotificationRankingUpdate nru = generateUpdate(getContext());
522         NotificationRankingUpdate nru2 = generateUpdate(getContext());
523         detailedAssertEquals(nru, nru2);
524         Assert.assertEquals(nru, nru2);
525         NotificationListenerService.Ranking tweak =
526                 nru2.getRankingMap().getRawRankingObject(mKeys[0]);
527         tweak.populate(
528                 tweak.getKey(),
529                 tweak.getRank(),
530                 !tweak.matchesInterruptionFilter(), // note the inversion here!
531                 tweak.getLockscreenVisibilityOverride(),
532                 tweak.getSuppressedVisualEffects(),
533                 tweak.getImportance(),
534                 tweak.getImportanceExplanation(),
535                 tweak.getOverrideGroupKey(),
536                 tweak.getChannel(),
537                 (ArrayList) tweak.getAdditionalPeople(),
538                 (ArrayList) tweak.getSnoozeCriteria(),
539                 tweak.canShowBadge(),
540                 tweak.getUserSentiment(),
541                 tweak.isSuspended(),
542                 tweak.getLastAudiblyAlertedMillis(),
543                 tweak.isNoisy(),
544                 (ArrayList) tweak.getSmartActions(),
545                 (ArrayList) tweak.getSmartReplies(),
546                 tweak.canBubble(),
547                 tweak.isTextChanged(),
548                 tweak.isConversation(),
549                 tweak.getConversationShortcutInfo(),
550                 tweak.getRankingAdjustment(),
551                 tweak.isBubble(),
552                 tweak.getProposedImportance(),
553                 tweak.hasSensitiveContent()
554         );
555         assertNotEquals(nru, nru2);
556     }
557 
558     @Test
testRankingUpdate_rankingConstructor()559     public void testRankingUpdate_rankingConstructor() {
560         NotificationRankingUpdate nru = generateUpdate(getContext());
561         NotificationRankingUpdate constructedNru = new NotificationRankingUpdate(
562                 new NotificationListenerService.Ranking[]{
563                         nru.getRankingMap().getRawRankingObject(mKeys[0]),
564                         nru.getRankingMap().getRawRankingObject(mKeys[1]),
565                         nru.getRankingMap().getRawRankingObject(mKeys[2]),
566                         nru.getRankingMap().getRawRankingObject(mKeys[3]),
567                         nru.getRankingMap().getRawRankingObject(mKeys[4])
568                 });
569 
570         detailedAssertEquals(nru, constructedNru);
571     }
572 
573     @Test
testRankingUpdate_emptyParcelInCheck()574     public void testRankingUpdate_emptyParcelInCheck() {
575         NotificationRankingUpdate rankingUpdate = generateUpdate(getContext());
576         Parcel parceledRankingUpdate = Parcel.obtain();
577         rankingUpdate.writeToParcel(parceledRankingUpdate, 0);
578 
579         // This will fail to read the parceledRankingUpdate, because the data position hasn't
580         // been reset, so it'll find no data to read.
581         NotificationRankingUpdate retrievedRankingUpdate = new NotificationRankingUpdate(
582                 parceledRankingUpdate);
583         assertNull(retrievedRankingUpdate.getRankingMap());
584         parceledRankingUpdate.recycle();
585     }
586 
587     @Test
testRankingUpdate_describeContents()588     public void testRankingUpdate_describeContents() {
589         NotificationRankingUpdate rankingUpdate = generateUpdate(getContext());
590         assertEquals(0, rankingUpdate.describeContents());
591     }
592 
593     @Test
testRankingUpdate_equals()594     public void testRankingUpdate_equals() {
595         NotificationListenerService.Ranking ranking = createEmptyTestRanking(TEST_KEY, 123, null);
596         NotificationRankingUpdate rankingUpdate = new NotificationRankingUpdate(
597                 new NotificationListenerService.Ranking[]{ranking});
598         // Reflexive equality, including handling nulls properly
599         detailedAssertEquals(rankingUpdate, rankingUpdate);
600         // Null or wrong class inequality
601         assertFalse(rankingUpdate.equals(null));
602         assertFalse(rankingUpdate.equals(ranking));
603 
604         // Different rank inequality
605         NotificationListenerService.Ranking ranking2 = createEmptyTestRanking(TEST_KEY, 456, null);
606         NotificationRankingUpdate rankingUpdate2 = new NotificationRankingUpdate(
607                 new NotificationListenerService.Ranking[]{ranking2});
608         assertFalse(rankingUpdate.equals(rankingUpdate2));
609 
610         // Different key inequality
611         ranking2 = createEmptyTestRanking(TEST_KEY + "DIFFERENT", 123, null);
612         rankingUpdate2 = new NotificationRankingUpdate(
613                 new NotificationListenerService.Ranking[]{ranking2});
614         assertFalse(rankingUpdate.equals(rankingUpdate2));
615     }
616 
617     @Test
testRankingUpdate_writesSmartActionToParcel()618     public void testRankingUpdate_writesSmartActionToParcel() {
619         if (!Flags.rankingUpdateAshmem()) {
620             return;
621         }
622         ArrayList<Notification.Action> actions = new ArrayList<>();
623         PendingIntent intent = PendingIntent.getBroadcast(
624                 getContext(),
625                 0 /*requestCode*/,
626                 new Intent("ACTION_" + TEST_KEY),
627                 PendingIntent.FLAG_IMMUTABLE /*flags*/);
628         actions.add(new Notification.Action.Builder(null /*icon*/, TEST_KEY, intent).build());
629 
630         NotificationListenerService.Ranking ranking =
631                 createEmptyTestRanking(TEST_KEY, 123, actions);
632         NotificationRankingUpdate rankingUpdate = new NotificationRankingUpdate(
633                 new NotificationListenerService.Ranking[]{ranking});
634 
635         Parcel parcel = Parcel.obtain();
636         rankingUpdate.writeToParcel(parcel, 0);
637         parcel.setDataPosition(0);
638         SharedMemory fd = parcel.readParcelable(getClass().getClassLoader(), SharedMemory.class);
639         Bundle smartActionsBundle = parcel.readBundle(getClass().getClassLoader());
640 
641         // Assert the file descriptor is valid
642         assertNotNull(fd);
643         assertFalse(fd.getFd() == -1);
644 
645         // Assert that the smart action is in the parcel
646         assertNotNull(smartActionsBundle);
647         ArrayList<Notification.Action> recoveredActions =
648                 smartActionsBundle.getParcelableArrayList(TEST_KEY, Notification.Action.class);
649         assertNotNull(recoveredActions);
650         assertEquals(actions.size(), recoveredActions.size());
651         assertEquals(actions.get(0).title.toString(), recoveredActions.get(0).title.toString());
652         parcel.recycle();
653     }
654 
655     @Test
testRankingUpdate_handlesEmptySmartActionList()656     public void testRankingUpdate_handlesEmptySmartActionList() {
657         if (!Flags.rankingUpdateAshmem()) {
658             return;
659         }
660         ArrayList<Notification.Action> actions = new ArrayList<>();
661         NotificationListenerService.Ranking ranking =
662                 createEmptyTestRanking(TEST_KEY, 123, actions);
663         NotificationRankingUpdate rankingUpdate = new NotificationRankingUpdate(
664                 new NotificationListenerService.Ranking[]{ranking});
665 
666         Parcel parcel = Parcel.obtain();
667         rankingUpdate.writeToParcel(parcel, 0);
668         parcel.setDataPosition(0);
669 
670         // Ensure that despite an empty actions list, we can still unparcel the update.
671         NotificationRankingUpdate newRankingUpdate = new NotificationRankingUpdate(parcel);
672         assertNotNull(newRankingUpdate);
673         assertNotNull(newRankingUpdate.getRankingMap());
674         detailedAssertEquals(rankingUpdate, newRankingUpdate);
675         parcel.recycle();
676     }
677 
678     @Test
testRankingUpdate_handlesNullSmartActionList()679     public void testRankingUpdate_handlesNullSmartActionList() {
680         if (!Flags.rankingUpdateAshmem()) {
681             return;
682         }
683         NotificationListenerService.Ranking ranking =
684                 createEmptyTestRanking(TEST_KEY, 123, null);
685         NotificationRankingUpdate rankingUpdate = new NotificationRankingUpdate(
686                 new NotificationListenerService.Ranking[]{ranking});
687 
688         Parcel parcel = Parcel.obtain();
689         rankingUpdate.writeToParcel(parcel, 0);
690         parcel.setDataPosition(0);
691 
692         // Ensure that despite an empty actions list, we can still unparcel the update.
693         NotificationRankingUpdate newRankingUpdate = new NotificationRankingUpdate(parcel);
694         assertNotNull(newRankingUpdate);
695         assertNotNull(newRankingUpdate.getRankingMap());
696         detailedAssertEquals(rankingUpdate, newRankingUpdate);
697         parcel.recycle();
698     }
699 }
700