1 /*
2  * Copyright (C) 2020 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 com.android.car.notification.utils;
18 
19 import static android.app.PendingIntent.FLAG_IMMUTABLE;
20 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
21 
22 import static androidx.core.app.NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ;
23 import static androidx.core.app.NotificationCompat.Action.SEMANTIC_ACTION_MUTE;
24 import static androidx.core.app.NotificationCompat.Action.SEMANTIC_ACTION_REPLY;
25 
26 import static org.mockito.Mockito.mock;
27 import static org.mockito.Mockito.when;
28 
29 import android.app.Notification;
30 import android.app.PendingIntent;
31 import android.app.Service;
32 import android.content.Context;
33 import android.content.Intent;
34 import android.graphics.drawable.Icon;
35 import android.os.Bundle;
36 import android.os.IBinder;
37 
38 import androidx.annotation.Nullable;
39 import androidx.core.app.NotificationCompat;
40 import androidx.core.app.NotificationCompat.Action;
41 import androidx.core.app.NotificationCompat.MessagingStyle;
42 import androidx.core.app.NotificationCompat.MessagingStyle.Message;
43 import androidx.core.app.Person;
44 import androidx.core.app.RemoteInput;
45 import androidx.core.graphics.drawable.IconCompat;
46 
47 import java.time.Instant;
48 
49 /**
50  * Builds {@link Notification}s for testing.
51  */
52 public class MockMessageNotificationBuilder {
53     private static final String SENDER_NAME = "senderName";
54     private static final String USER_NAME = "userName";
55     private static final String TITLE_MARK_AS_READ = "Mark As Read";
56     private static final String TITLE_REPLY = "Reply";
57     private static final String TITLE_MUTE= "Mute";
58     private static final String CONTENT_TITLE_PREFIX = "2 new messages from ";
59     private static final String CONTENT_SUBJECT = "subject";
60     private static final String OLD_MESSAGE = "old message";
61     private static final String FIRST_MESSAGE = "first message";
62     private static final String SECOND_MESSAGE = "second message";
63 
64     private final Context mContext;
65     private final Icon mActionIcon;
66     private final Person mSender;
67     private final Person mUser;
68     private final String mChannelId;
69     private final int mSmallIconId;
70     @Nullable
71     private Message mPostSpecificMessage = null;
72     @Nullable
73     private Bundle mExtras = null;
74     private boolean mHasMessagingStyle = false;
75     private boolean mIsOldMessage = false;
76     private boolean mHasReplyAction = false;
77     private boolean mReplyActionIsMocked = false;
78     private boolean mMuteActionIsMocked = false;
79     private boolean mMarkAsReadActionIsMocked = false;
80     private boolean mPendingIntentIsMocked = false;
81     private boolean mHasReplyWrongSemanticAction = false;
82     private boolean mHasMarkAsRead = false;
83     private boolean mHasMute = false;
84     private boolean mUseInvisibleAction = false;
85     private boolean mShowsUI = false;
86     private Instant mConnectionTime = Instant.now();
87     private String mContentTitle = "";
88     private String mCategory = "";
89     private String mContentText = "";
90     private Action mReplyAction = null;
91     private Action mMuteAction = null;
92     private Action mMarkAsReadAction = null;
93     private PendingIntent mPendingIntent = null;
94 
MockMessageNotificationBuilder(Context context, String channelId, int smallIconId)95     public MockMessageNotificationBuilder(Context context, String channelId, int smallIconId) {
96         mContext = context;
97         mChannelId = channelId;
98         mSmallIconId = smallIconId;
99         mActionIcon = Icon.createWithResource(mContext, /* resId= */ 1);
100         mSender = (new Person.Builder()).setName(SENDER_NAME).build();
101         mUser = (new Person.Builder()).setName(USER_NAME).build();
102     }
103 
build()104     public Notification build() {
105         NotificationCompat.Builder builder = new NotificationCompat.Builder(this.mContext,
106                 mChannelId).setSmallIcon(mSmallIconId).setContentText(
107                 CONTENT_SUBJECT);
108 
109         if (mExtras != null) {
110             builder.setExtras(mExtras);
111         }
112 
113         if (!mContentTitle.isEmpty()) {
114             builder.setContentTitle(mContentTitle);
115         }
116 
117         if (!mContentText.isEmpty()) {
118             builder.setContentText(mContentText);
119         }
120 
121         if (!mCategory.isEmpty()) {
122             builder.setCategory(mCategory);
123         }
124 
125         if (mHasMessagingStyle) {
126             builder.setStyle(buildStyle());
127         }
128 
129         if (mPendingIntentIsMocked) {
130             mPendingIntent = mock(PendingIntent.class);
131             when(mPendingIntent.describeContents()).thenReturn(0);
132         } else {
133             mPendingIntent = PendingIntent.getService(mContext, /* requestCode= */ 101,
134                     new Intent(mContext, MockService.class), FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE);
135         }
136 
137         if (mHasReplyAction) {
138             if (mReplyActionIsMocked) {
139                 mReplyAction = getMockReplyAction(
140                         mHasReplyWrongSemanticAction ? Action.SEMANTIC_ACTION_NONE
141                                 : SEMANTIC_ACTION_REPLY);
142             } else {
143                 mReplyAction = getNonMockReplyAction(
144                         mHasReplyWrongSemanticAction ? Action.SEMANTIC_ACTION_NONE
145                                 : SEMANTIC_ACTION_REPLY);
146             }
147 
148             if (mUseInvisibleAction) {
149                 builder.addInvisibleAction(mReplyAction);
150             } else {
151                 builder.addAction(mReplyAction);
152             }
153         }
154 
155         if (mHasMarkAsRead) {
156             if (mMarkAsReadActionIsMocked) {
157                 mMarkAsReadAction = getMockMarkAsReadAction();
158             } else {
159                 mMarkAsReadAction = getNonMockMarkAsReadAction();
160             }
161 
162             if (mUseInvisibleAction) {
163                 builder.addInvisibleAction(mMarkAsReadAction);
164             } else {
165                 builder.addAction(mMarkAsReadAction);
166             }
167         }
168 
169         if (mHasMute) {
170             if (mMuteActionIsMocked) {
171                 mMuteAction = getMockMuteAction();
172             } else {
173                 mMuteAction = getNonMockMuteAction();
174             }
175 
176             if (mUseInvisibleAction) {
177                 builder.addInvisibleAction(mMuteAction);
178             } else {
179                 builder.addAction(mMuteAction);
180             }
181         }
182 
183         return builder.build();
184     }
185 
setHasMessagingStyle(boolean hasMessagingStyle)186     public MockMessageNotificationBuilder setHasMessagingStyle(boolean hasMessagingStyle) {
187         mHasMessagingStyle = hasMessagingStyle;
188         return this;
189     }
190 
setOldMessage(boolean oldMessage)191     public MockMessageNotificationBuilder setOldMessage(boolean oldMessage) {
192         mIsOldMessage = oldMessage;
193         return this;
194     }
195 
setHasReplyAction(boolean hasReplyAction)196     public MockMessageNotificationBuilder setHasReplyAction(boolean hasReplyAction) {
197         mHasReplyAction = hasReplyAction;
198         return this;
199     }
200 
setReplyActionIsMocked(boolean replyActionIsMocked)201     public MockMessageNotificationBuilder setReplyActionIsMocked(boolean replyActionIsMocked) {
202         mReplyActionIsMocked = replyActionIsMocked;
203         return this;
204     }
205 
setMarkAsReadActionIsMocked( boolean markAsReadActionIsMocked)206     public MockMessageNotificationBuilder setMarkAsReadActionIsMocked(
207             boolean markAsReadActionIsMocked) {
208         mMarkAsReadActionIsMocked = markAsReadActionIsMocked;
209         return this;
210     }
211 
setMuteActionIsMocked( boolean muteActionIsMocked)212     public MockMessageNotificationBuilder setMuteActionIsMocked(
213             boolean muteActionIsMocked) {
214         mMuteActionIsMocked = muteActionIsMocked;
215         return this;
216     }
217 
setPendingIntentIsMocked(boolean pendingIntentIsMocked)218     public MockMessageNotificationBuilder setPendingIntentIsMocked(boolean pendingIntentIsMocked) {
219         mPendingIntentIsMocked = pendingIntentIsMocked;
220         return this;
221     }
222 
setHasReplyWrongSemanticAction( boolean hasReplyWrongSemanticAction)223     public MockMessageNotificationBuilder setHasReplyWrongSemanticAction(
224             boolean hasReplyWrongSemanticAction) {
225         mHasReplyWrongSemanticAction = hasReplyWrongSemanticAction;
226         return this;
227     }
228 
setHasMarkAsRead(boolean hasMarkAsRead)229     public MockMessageNotificationBuilder setHasMarkAsRead(boolean hasMarkAsRead) {
230         mHasMarkAsRead = hasMarkAsRead;
231         return this;
232     }
233 
setHasMuteAction(boolean hasMute)234     public MockMessageNotificationBuilder setHasMuteAction(boolean hasMute) {
235         mHasMute = hasMute;
236         return this;
237     }
238 
setUseInvisibleAction(boolean useInvisibleAction)239     public MockMessageNotificationBuilder setUseInvisibleAction(boolean useInvisibleAction) {
240         mUseInvisibleAction = useInvisibleAction;
241         return this;
242     }
243 
setShowsUI(boolean showsUI)244     public MockMessageNotificationBuilder setShowsUI(boolean showsUI) {
245         mShowsUI = showsUI;
246         return this;
247     }
248 
setConnectionTime(Instant connectionTime)249     public MockMessageNotificationBuilder setConnectionTime(Instant connectionTime) {
250         mConnectionTime = connectionTime;
251         return this;
252     }
253 
setPostSpecificMessage( MessagingStyle.Message postSpecificMessage)254     public MockMessageNotificationBuilder setPostSpecificMessage(
255             MessagingStyle.Message postSpecificMessage) {
256         mPostSpecificMessage = postSpecificMessage;
257         return this;
258     }
259 
setContentTitle(String contentTitle)260     public MockMessageNotificationBuilder setContentTitle(String contentTitle) {
261         mContentTitle = contentTitle;
262         return this;
263     }
264 
setCategory(String category)265     public MockMessageNotificationBuilder setCategory(String category) {
266         mCategory = category;
267         return this;
268     }
269 
setContentText(String contentText)270     public MockMessageNotificationBuilder setContentText(String contentText) {
271         mContentText = contentText;
272         return this;
273     }
274 
setExtras(@ullable Bundle extras)275     public MockMessageNotificationBuilder setExtras(@Nullable Bundle extras) {
276         mExtras = extras;
277         return this;
278     }
279 
getReplyAction()280     public Action getReplyAction() {
281         return mReplyAction;
282     }
283 
getMuteAction()284     public Action getMuteAction() {
285         return mMuteAction;
286     }
287 
getMarkAsReadAction()288     public Action getMarkAsReadAction() {
289         return mMarkAsReadAction;
290     }
291 
getPendingIntent()292     public PendingIntent getPendingIntent() {
293         return mPendingIntent;
294     }
295 
buildStyle()296     private MessagingStyle buildStyle() {
297         MessagingStyle builder = new MessagingStyle(mUser);
298         long connectionTimeMs = mConnectionTime.toEpochMilli();
299         if (mPostSpecificMessage != null) {
300             return builder.addMessage(mPostSpecificMessage);
301         } else if (mIsOldMessage) {
302             return builder.addMessage(OLD_MESSAGE, /* timestamp= */ connectionTimeMs - 100,
303                     mSender);
304         } else {
305             return builder.addMessage(FIRST_MESSAGE, /* timestamp= */ connectionTimeMs + 100,
306                     mSender).addMessage(SECOND_MESSAGE, /* timestamp= */ connectionTimeMs + 100,
307                     mSender);
308         }
309     }
310 
getNonMockReplyAction(int semanticAction)311     private Action getNonMockReplyAction(int semanticAction) {
312         return (new Action.Builder(IconCompat.createFromIcon(mActionIcon), TITLE_REPLY,
313                 mPendingIntent)).addRemoteInput(
314                 new RemoteInput.Builder(mChannelId).build()).setSemanticAction(
315                 semanticAction).setShowsUserInterface(mShowsUI).build();
316     }
317 
getMockReplyAction(int semanticAction)318     private Action getMockReplyAction(int semanticAction) {
319         IconCompat iconCompat = IconCompat.createFromIcon(mActionIcon);
320         Action action = mock(Action.class);
321         when(action.actionIntent).thenReturn(mPendingIntent);
322         when(action.title).thenReturn(TITLE_REPLY);
323         when(action.icon).thenReturn(iconCompat.getResId());
324         when(action.getActionIntent()).thenReturn(mPendingIntent);
325         when(action.getAllowGeneratedReplies()).thenReturn(true);
326         when(action.getDataOnlyRemoteInputs()).thenReturn(null);
327         when(action.getExtras()).thenReturn(new Bundle());
328         when(action.getRemoteInputs()).thenReturn(new RemoteInput[]{new RemoteInput.Builder(
329                 mChannelId).build()});
330         when(action.getSemanticAction()).thenReturn(semanticAction);
331         when(action.getShowsUserInterface()).thenReturn(mShowsUI);
332         when(action.getTitle()).thenReturn(TITLE_REPLY);
333         when(action.isContextual()).thenReturn(false);
334         when(action.getIconCompat()).thenReturn(iconCompat);
335         return action;
336     }
337 
getNonMockMarkAsReadAction()338     private Action getNonMockMarkAsReadAction() {
339         return (new Action.Builder(IconCompat.createFromIcon(mActionIcon),
340                 TITLE_MARK_AS_READ, mPendingIntent)).addRemoteInput(
341                 new RemoteInput.Builder(mChannelId).build()).setSemanticAction(
342                 SEMANTIC_ACTION_MARK_AS_READ).setShowsUserInterface(false).build();
343     }
344 
getMockMarkAsReadAction()345     private Action getMockMarkAsReadAction() {
346         IconCompat iconCompat = IconCompat.createFromIcon(mActionIcon);
347         Action action = mock(Action.class);
348         when(action.actionIntent).thenReturn(mPendingIntent);
349         when(action.title).thenReturn(TITLE_MARK_AS_READ);
350         when(action.icon).thenReturn(iconCompat.getResId());
351         when(action.getActionIntent()).thenReturn(mPendingIntent);
352         when(action.getAllowGeneratedReplies()).thenReturn(true);
353         when(action.getDataOnlyRemoteInputs()).thenReturn(null);
354         when(action.getExtras()).thenReturn(new Bundle());
355         when(action.getRemoteInputs()).thenReturn(new RemoteInput[]{new RemoteInput.Builder(
356                 mChannelId).build()});
357         when(action.getSemanticAction()).thenReturn(SEMANTIC_ACTION_MARK_AS_READ);
358         when(action.getShowsUserInterface()).thenReturn(false);
359         when(action.getTitle()).thenReturn(TITLE_MARK_AS_READ);
360         when(action.isContextual()).thenReturn(false);
361         when(action.getIconCompat()).thenReturn(iconCompat);
362         return action;
363     }
364 
getNonMockMuteAction()365     private Action getNonMockMuteAction() {
366         return (new Action.Builder(IconCompat.createFromIcon(mActionIcon),
367                 TITLE_MUTE, mPendingIntent)).addRemoteInput(
368                 new RemoteInput.Builder(mChannelId).build()).setSemanticAction(
369                 SEMANTIC_ACTION_MUTE).setShowsUserInterface(false).build();
370     }
371 
getMockMuteAction()372     private Action getMockMuteAction() {
373         IconCompat iconCompat = IconCompat.createFromIcon(mActionIcon);
374         Action action = mock(Action.class);
375         when(action.actionIntent).thenReturn(mPendingIntent);
376         when(action.title).thenReturn(TITLE_MUTE);
377         when(action.icon).thenReturn(iconCompat.getResId());
378         when(action.getActionIntent()).thenReturn(mPendingIntent);
379         when(action.getAllowGeneratedReplies()).thenReturn(true);
380         when(action.getDataOnlyRemoteInputs()).thenReturn(null);
381         when(action.getExtras()).thenReturn(new Bundle());
382         when(action.getRemoteInputs()).thenReturn(new RemoteInput[]{new RemoteInput.Builder(
383                 mChannelId).build()});
384         when(action.getSemanticAction()).thenReturn(SEMANTIC_ACTION_MUTE);
385         when(action.getShowsUserInterface()).thenReturn(false);
386         when(action.getTitle()).thenReturn(TITLE_MUTE);
387         when(action.isContextual()).thenReturn(false);
388         when(action.getIconCompat()).thenReturn(iconCompat);
389         return action;
390     }
391 
392     private static class MockService extends Service {
393         @Nullable
394         @Override
onBind(Intent intent)395         public IBinder onBind(Intent intent) {
396             return null;
397         }
398     }
399 }