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 }