1/*
2 * Copyright (C) 2017 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
17package android.app;
18
19import static com.android.internal.util.NotificationColorUtil.satisfiesTextContrast;
20
21import static org.junit.Assert.assertEquals;
22import static org.junit.Assert.assertFalse;
23import static org.junit.Assert.assertNotSame;
24import static org.junit.Assert.assertSame;
25import static org.junit.Assert.assertTrue;
26
27import android.annotation.Nullable;
28import android.content.Context;
29import android.content.Intent;
30import android.graphics.BitmapFactory;
31import android.graphics.drawable.Icon;
32import android.media.session.MediaSession;
33import android.os.Build;
34import android.os.Parcel;
35import android.os.Parcelable;
36import android.support.test.InstrumentationRegistry;
37import android.support.test.filters.SmallTest;
38import android.support.test.runner.AndroidJUnit4;
39import android.widget.RemoteViews;
40
41import org.junit.Before;
42import org.junit.Test;
43import org.junit.runner.RunWith;
44
45import java.util.function.Consumer;
46
47@RunWith(AndroidJUnit4.class)
48@SmallTest
49public class NotificationTest {
50
51    private Context mContext;
52
53    @Before
54    public void setUp() {
55        mContext = InstrumentationRegistry.getContext();
56    }
57
58    @Test
59    public void testColorizedByPermission() {
60        Notification n = new Notification.Builder(mContext, "test")
61                .setFlag(Notification.FLAG_CAN_COLORIZE, true)
62                .setColorized(true)
63                .build();
64        assertTrue(n.isColorized());
65
66        n = new Notification.Builder(mContext, "test")
67                .setFlag(Notification.FLAG_CAN_COLORIZE, true)
68                .build();
69        assertFalse(n.isColorized());
70
71        n = new Notification.Builder(mContext, "test")
72                .setFlag(Notification.FLAG_CAN_COLORIZE, false)
73                .setColorized(true)
74                .build();
75        assertFalse(n.isColorized());
76    }
77
78    @Test
79    public void testColorizedByForeground() {
80        Notification n = new Notification.Builder(mContext, "test")
81                .setFlag(Notification.FLAG_FOREGROUND_SERVICE, true)
82                .setColorized(true)
83                .build();
84        assertTrue(n.isColorized());
85
86        n = new Notification.Builder(mContext, "test")
87                .setFlag(Notification.FLAG_FOREGROUND_SERVICE, true)
88                .build();
89        assertFalse(n.isColorized());
90
91        n = new Notification.Builder(mContext, "test")
92                .setFlag(Notification.FLAG_FOREGROUND_SERVICE, false)
93                .setColorized(true)
94                .build();
95        assertFalse(n.isColorized());
96    }
97
98    @Test
99    public void testColorSatisfiedWhenBgDarkTextDarker() {
100        Notification.Builder builder = getMediaNotification();
101        Notification n = builder.build();
102
103        assertTrue(n.isColorized());
104
105        // An initial guess where the foreground color is actually darker than an already dark bg
106        int backgroundColor = 0xff585868;
107        int initialForegroundColor = 0xff505868;
108        builder.setColorPalette(backgroundColor, initialForegroundColor);
109        int primaryTextColor = builder.getPrimaryTextColor();
110        assertTrue(satisfiesTextContrast(primaryTextColor, backgroundColor));
111        int secondaryTextColor = builder.getSecondaryTextColor();
112        assertTrue(satisfiesTextContrast(secondaryTextColor, backgroundColor));
113    }
114
115    @Test
116    public void testHasCompletedProgress_noProgress() {
117        Notification n = new Notification.Builder(mContext).build();
118
119        assertFalse(n.hasCompletedProgress());
120    }
121
122    @Test
123    public void testHasCompletedProgress_complete() {
124        Notification n = new Notification.Builder(mContext)
125                .setProgress(100, 100, true)
126                .build();
127        Notification n2 = new Notification.Builder(mContext)
128                .setProgress(10, 10, false)
129                .build();
130        assertTrue(n.hasCompletedProgress());
131        assertTrue(n2.hasCompletedProgress());
132    }
133
134    @Test
135    public void testHasCompletedProgress_notComplete() {
136        Notification n = new Notification.Builder(mContext)
137                .setProgress(100, 99, true)
138                .build();
139        Notification n2 = new Notification.Builder(mContext)
140                .setProgress(10, 4, false)
141                .build();
142        assertFalse(n.hasCompletedProgress());
143        assertFalse(n2.hasCompletedProgress());
144    }
145
146    @Test
147    public void testHasCompletedProgress_zeroMax() {
148        Notification n = new Notification.Builder(mContext)
149                .setProgress(0, 0, true)
150                .build();
151        assertFalse(n.hasCompletedProgress());
152    }
153
154    @Test
155    public void largeIconMultipleReferences_keptAfterParcelling() {
156        Icon originalIcon = Icon.createWithBitmap(BitmapFactory.decodeResource(
157                mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96));
158
159        Notification n = new Notification.Builder(mContext).setLargeIcon(originalIcon).build();
160        assertSame(n.getLargeIcon(), originalIcon);
161
162        Notification q = writeAndReadParcelable(n);
163        assertNotSame(q.getLargeIcon(), n.getLargeIcon());
164
165        assertTrue(q.getLargeIcon().getBitmap().sameAs(n.getLargeIcon().getBitmap()));
166        assertSame(q.getLargeIcon(), q.extras.getParcelable(Notification.EXTRA_LARGE_ICON));
167    }
168
169    @Test
170    public void largeIconReferenceInExtrasOnly_keptAfterParcelling() {
171        Icon originalIcon = Icon.createWithBitmap(BitmapFactory.decodeResource(
172                mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96));
173
174        Notification n = new Notification.Builder(mContext).build();
175        n.extras.putParcelable(Notification.EXTRA_LARGE_ICON, originalIcon);
176        assertSame(n.getLargeIcon(), null);
177
178        Notification q = writeAndReadParcelable(n);
179        assertSame(q.getLargeIcon(), null);
180        assertTrue(((Icon) q.extras.getParcelable(Notification.EXTRA_LARGE_ICON)).getBitmap()
181                .sameAs(originalIcon.getBitmap()));
182    }
183
184    @Test
185    public void allPendingIntents_recollectedAfterReusingBuilder() {
186        PendingIntent intent1 = PendingIntent.getActivity(mContext, 0, new Intent("test1"), 0);
187        PendingIntent intent2 = PendingIntent.getActivity(mContext, 0, new Intent("test2"), 0);
188
189        Notification.Builder builder = new Notification.Builder(mContext, "channel");
190        builder.setContentIntent(intent1);
191
192        Parcel p = Parcel.obtain();
193
194        Notification n1 = builder.build();
195        n1.writeToParcel(p, 0);
196
197        builder.setContentIntent(intent2);
198        Notification n2 = builder.build();
199        n2.writeToParcel(p, 0);
200
201        assertTrue(n2.allPendingIntents.contains(intent2));
202    }
203
204    @Test
205    public void allPendingIntents_containsCustomRemoteViews() {
206        PendingIntent intent = PendingIntent.getActivity(mContext, 0, new Intent("test"), 0);
207
208        RemoteViews contentView = new RemoteViews(mContext.getPackageName(), 0 /* layoutId */);
209        contentView.setOnClickPendingIntent(1 /* id */, intent);
210
211        Notification.Builder builder = new Notification.Builder(mContext, "channel");
212        builder.setCustomContentView(contentView);
213
214        Parcel p = Parcel.obtain();
215
216        Notification n = builder.build();
217        n.writeToParcel(p, 0);
218
219        assertTrue(n.allPendingIntents.contains(intent));
220    }
221
222    @Test
223    public void messagingStyle_isGroupConversation() {
224        mContext.getApplicationInfo().targetSdkVersion = Build.VERSION_CODES.P;
225        Notification.MessagingStyle messagingStyle = new Notification.MessagingStyle("self name")
226                .setGroupConversation(true)
227                .setConversationTitle("test conversation title");
228        Notification notification = new Notification.Builder(mContext, "test id")
229                .setSmallIcon(1)
230                .setContentTitle("test title")
231                .setStyle(messagingStyle)
232                .build();
233
234        assertTrue(messagingStyle.isGroupConversation());
235        assertTrue(notification.extras.getBoolean(Notification.EXTRA_IS_GROUP_CONVERSATION));
236    }
237
238    @Test
239    public void messagingStyle_isGroupConversation_noConversationTitle() {
240        mContext.getApplicationInfo().targetSdkVersion = Build.VERSION_CODES.P;
241        Notification.MessagingStyle messagingStyle = new Notification.MessagingStyle("self name")
242                .setGroupConversation(true)
243                .setConversationTitle(null);
244        Notification notification = new Notification.Builder(mContext, "test id")
245                .setSmallIcon(1)
246                .setContentTitle("test title")
247                .setStyle(messagingStyle)
248                .build();
249
250        assertTrue(messagingStyle.isGroupConversation());
251        assertTrue(notification.extras.getBoolean(Notification.EXTRA_IS_GROUP_CONVERSATION));
252    }
253
254    @Test
255    public void messagingStyle_isGroupConversation_withConversationTitle_legacy() {
256        // In legacy (version < P), isGroupConversation is controlled by conversationTitle.
257        mContext.getApplicationInfo().targetSdkVersion = Build.VERSION_CODES.O;
258        Notification.MessagingStyle messagingStyle = new Notification.MessagingStyle("self name")
259                .setGroupConversation(false)
260                .setConversationTitle("test conversation title");
261        Notification notification = new Notification.Builder(mContext, "test id")
262                .setSmallIcon(1)
263                .setContentTitle("test title")
264                .setStyle(messagingStyle)
265                .build();
266
267        assertTrue(messagingStyle.isGroupConversation());
268        assertFalse(notification.extras.getBoolean(Notification.EXTRA_IS_GROUP_CONVERSATION));
269    }
270
271    @Test
272    public void messagingStyle_isGroupConversation_withoutConversationTitle_legacy() {
273        // In legacy (version < P), isGroupConversation is controlled by conversationTitle.
274        mContext.getApplicationInfo().targetSdkVersion = Build.VERSION_CODES.O;
275        Notification.MessagingStyle messagingStyle = new Notification.MessagingStyle("self name")
276                .setGroupConversation(true)
277                .setConversationTitle(null);
278        Notification notification = new Notification.Builder(mContext, "test id")
279                .setSmallIcon(1)
280                .setContentTitle("test title")
281                .setStyle(messagingStyle)
282                .build();
283
284        assertFalse(messagingStyle.isGroupConversation());
285        assertTrue(notification.extras.getBoolean(Notification.EXTRA_IS_GROUP_CONVERSATION));
286    }
287
288    @Test
289    public void action_builder_hasDefault() {
290        Notification.Action action = makeNotificationAction(null);
291        assertEquals(Notification.Action.SEMANTIC_ACTION_NONE, action.getSemanticAction());
292    }
293
294    @Test
295    public void action_builder_setSemanticAction() {
296        Notification.Action action = makeNotificationAction(
297                builder -> builder.setSemanticAction(Notification.Action.SEMANTIC_ACTION_REPLY));
298        assertEquals(Notification.Action.SEMANTIC_ACTION_REPLY, action.getSemanticAction());
299    }
300
301    @Test
302    public void action_parcel() {
303        Notification.Action action = writeAndReadParcelable(
304                makeNotificationAction(builder -> {
305                    builder.setSemanticAction(Notification.Action.SEMANTIC_ACTION_ARCHIVE);
306                    builder.setAllowGeneratedReplies(true);
307                }));
308
309        assertEquals(Notification.Action.SEMANTIC_ACTION_ARCHIVE, action.getSemanticAction());
310        assertTrue(action.getAllowGeneratedReplies());
311    }
312
313    @Test
314    public void action_clone() {
315        Notification.Action action = makeNotificationAction(
316                builder -> builder.setSemanticAction(Notification.Action.SEMANTIC_ACTION_DELETE));
317        assertEquals(
318                Notification.Action.SEMANTIC_ACTION_DELETE,
319                action.clone().getSemanticAction());
320    }
321
322    private Notification.Builder getMediaNotification() {
323        MediaSession session = new MediaSession(mContext, "test");
324        return new Notification.Builder(mContext, "color")
325                .setSmallIcon(com.android.internal.R.drawable.emergency_icon)
326                .setContentTitle("Title")
327                .setContentText("Text")
328                .setStyle(new Notification.MediaStyle().setMediaSession(session.getSessionToken()));
329    }
330
331    /**
332      * Writes an arbitrary {@link Parcelable} into a {@link Parcel} using its writeToParcel
333      * method before reading it out again to check that it was sent properly.
334      */
335    private static <T extends Parcelable> T writeAndReadParcelable(T original) {
336        Parcel p = Parcel.obtain();
337        p.writeParcelable(original, /* flags */ 0);
338        p.setDataPosition(0);
339        return p.readParcelable(/* classLoader */ null);
340    }
341
342    /**
343     * Creates a Notification.Action by mocking initial dependencies and then applying
344     * transformations if they're defined.
345     */
346    private Notification.Action makeNotificationAction(
347            @Nullable Consumer<Notification.Action.Builder> transformation) {
348        Notification.Action.Builder actionBuilder =
349                new Notification.Action.Builder(null, "Test Title", null);
350        if (transformation != null) {
351            transformation.accept(actionBuilder);
352        }
353        return actionBuilder.build();
354    }
355}
356