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