1 /* 2 * Copyright (C) 2016 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.notification.functional; 18 19 import static org.junit.Assert.assertFalse; 20 import static org.junit.Assert.assertTrue; 21 22 import android.app.AppOpsManager; 23 import android.app.Notification; 24 import android.app.NotificationChannel; 25 import android.app.NotificationManager; 26 import android.app.PendingIntent; 27 import android.app.usage.UsageEvents; 28 import android.app.usage.UsageEvents.Event; 29 import android.app.usage.UsageStatsManager; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.metrics.LogMaker; 33 import android.provider.Settings; 34 import android.service.notification.StatusBarNotification; 35 import android.support.test.metricshelper.MetricsAsserts; 36 import android.support.test.uiautomator.By; 37 import android.support.test.uiautomator.Direction; 38 import android.support.test.uiautomator.UiDevice; 39 import android.support.test.uiautomator.UiObject2; 40 import android.support.test.uiautomator.Until; 41 import android.test.InstrumentationTestCase; 42 import android.test.suitebuilder.annotation.MediumTest; 43 import android.util.Log; 44 45 import android.metrics.MetricsReader; 46 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 47 48 import java.text.MessageFormat; 49 import java.util.ArrayList; 50 import java.util.Arrays; 51 import java.util.HashMap; 52 import java.util.List; 53 import java.util.Map; 54 import java.util.Queue; 55 import org.junit.Test; 56 57 public class NotificationInteractionTests extends InstrumentationTestCase { 58 private static final String LOG_TAG = NotificationInteractionTests.class.getSimpleName(); 59 private static final int LONG_TIMEOUT = 3000; 60 private static final int SHORT_TIMEOUT = 200; 61 private static final int GROUP_NOTIFICATION_ID = 1; 62 private static final int CHILD_NOTIFICATION_ID = 100; 63 private static final int SECOND_CHILD_NOTIFICATION_ID = 101; 64 private static final String BUNDLE_GROUP_KEY = "group key "; 65 private static final String CHANNEL_ID = "my_channel"; 66 private final boolean DEBUG = false; 67 private static final String APPOPS_SET_SHELL_COMMAND = "appops set {0} " + 68 AppOpsManager.OPSTR_GET_USAGE_STATS + " {1}"; 69 private NotificationManager mNotificationManager; 70 private UiDevice mDevice = null; 71 private Context mContext; 72 private NotificationHelper mHelper; 73 private static final int CUSTOM_NOTIFICATION_ID = 1; 74 private static final int NOTIFICATIONS_COUNT = 3; 75 private MetricsReader mMetricsReader; 76 77 @Override setUp()78 public void setUp() throws Exception { 79 super.setUp(); 80 mDevice = UiDevice.getInstance(getInstrumentation()); 81 mContext = getInstrumentation().getContext(); 82 mNotificationManager = (NotificationManager) mContext 83 .getSystemService(Context.NOTIFICATION_SERVICE); 84 mHelper = new NotificationHelper(mDevice, getInstrumentation(), mNotificationManager); 85 mDevice.setOrientationNatural(); 86 mNotificationManager.cancelAll(); 87 mMetricsReader = new MetricsReader(); 88 mMetricsReader.checkpoint(); // clear out old logs 89 } 90 91 @Override tearDown()92 public void tearDown() throws Exception { 93 super.tearDown(); 94 mDevice.unfreezeRotation(); 95 mDevice.pressHome(); 96 mNotificationManager.cancelAll(); 97 } 98 99 @MediumTest testNonDismissNotification()100 public void testNonDismissNotification() throws Exception { 101 String text = "USB debugging connected"; 102 mDevice.openNotification(); 103 Thread.sleep(LONG_TIMEOUT); 104 UiObject2 obj = findByText(text); 105 assertNotNull(String.format("Couldn't find %s notification", text), obj); 106 obj.swipe(Direction.LEFT, 1.0f); 107 Thread.sleep(LONG_TIMEOUT); 108 obj = mDevice.wait(Until.findObject(By.text(text)), 109 LONG_TIMEOUT); 110 assertNotNull("USB debugging notification has been dismissed", obj); 111 } 112 113 /** send out multiple notifications in order to test CLEAR ALL function */ 114 @MediumTest testDismissAll()115 public void testDismissAll() throws Exception { 116 String text = "Clear all"; 117 Map<Integer, String> lists = new HashMap<Integer, String>(); 118 StatusBarNotification[] sbns = mNotificationManager.getActiveNotifications(); 119 int currentSbns = sbns.length; 120 for (int i = 0; i < NOTIFICATIONS_COUNT; i++) { 121 lists.put(CUSTOM_NOTIFICATION_ID + i, Integer.toString(CUSTOM_NOTIFICATION_ID + i)); 122 } 123 mHelper.sendNotifications(lists, false); 124 125 if (DEBUG) { 126 Log.d(LOG_TAG, 127 String.format("posted %s notifications, here they are: ", NOTIFICATIONS_COUNT)); 128 sbns = mNotificationManager.getActiveNotifications(); 129 for (StatusBarNotification sbn : sbns) { 130 Log.d(LOG_TAG, " " + sbn); 131 } 132 } 133 if (mDevice.openNotification()) { 134 Thread.sleep(LONG_TIMEOUT); 135 UiObject2 clearAll = findByText(text); 136 assertNotNull("could not find clear all target", clearAll); 137 clearAll.click(); 138 } 139 Thread.sleep(LONG_TIMEOUT); 140 sbns = mNotificationManager.getActiveNotifications(); 141 assertTrue(String.format("%s notifications have not been cleared", sbns.length), 142 sbns.length == currentSbns); 143 MetricsAsserts.assertHasLog("missing notification cancel log", mMetricsReader, 144 new LogMaker(MetricsEvent.NOTIFICATION_ITEM) 145 .setType(MetricsEvent.TYPE_DISMISS) 146 .addTaggedData(MetricsEvent.NOTIFICATION_ID, CUSTOM_NOTIFICATION_ID) 147 .setPackageName(mContext.getPackageName())); 148 MetricsAsserts.assertHasActionLog("missing dismiss-all log", mMetricsReader, 149 MetricsEvent.ACTION_DISMISS_ALL_NOTES); 150 } 151 152 /** send notifications, then open and close the shade to test visibility metrics. */ 153 @MediumTest testNotificationShadeMetrics()154 public void testNotificationShadeMetrics() throws Exception { 155 Map<Integer, String> lists = new HashMap<Integer, String>(); 156 int firstId = CUSTOM_NOTIFICATION_ID; 157 int secondId = CUSTOM_NOTIFICATION_ID + 1; 158 lists.put(firstId, Integer.toString(firstId)); 159 lists.put(secondId, Integer.toString(secondId)); 160 // post 161 mHelper.sendNotifications(lists, true); 162 Thread.sleep(LONG_TIMEOUT); 163 // update 164 mHelper.sendNotifications(lists, true); 165 166 if (mDevice.openNotification()) { 167 Thread.sleep(LONG_TIMEOUT); 168 } 169 MetricsAsserts.assertHasVisibilityLog("missing panel revealed log", mMetricsReader, 170 MetricsEvent.NOTIFICATION_PANEL, true); 171 Queue<LogMaker> firstLog = MetricsAsserts.findMatchingLogs(mMetricsReader, 172 new LogMaker(MetricsEvent.NOTIFICATION_ITEM) 173 .setType(MetricsEvent.TYPE_OPEN) 174 .addTaggedData(MetricsEvent.NOTIFICATION_ID, firstId) 175 .setPackageName(mContext.getPackageName())); 176 assertTrue("missing first note visibility log", !firstLog.isEmpty()); 177 Queue<LogMaker> secondLog = MetricsAsserts.findMatchingLogs(mMetricsReader, 178 new LogMaker(MetricsEvent.NOTIFICATION_ITEM) 179 .setType(MetricsEvent.TYPE_OPEN) 180 .addTaggedData(MetricsEvent.NOTIFICATION_ID, secondId)); 181 assertTrue("missing second note visibility log", !secondLog.isEmpty()); 182 int firstRank = (Integer) firstLog.peek() 183 .getTaggedData(MetricsEvent.NOTIFICATION_SHADE_INDEX); 184 int secondRank = (Integer) secondLog.peek() 185 .getTaggedData(MetricsEvent.NOTIFICATION_SHADE_INDEX); 186 assertTrue("note must have distinct ranks", firstRank != secondRank); 187 int lifespan = (Integer) firstLog.peek() 188 .getTaggedData(MetricsEvent.NOTIFICATION_SINCE_CREATE_MILLIS); 189 int freshness = (Integer) firstLog.peek() 190 .getTaggedData(MetricsEvent.NOTIFICATION_SINCE_UPDATE_MILLIS); 191 int exposure = (Integer) firstLog.peek() 192 .getTaggedData(MetricsEvent.NOTIFICATION_SINCE_VISIBLE_MILLIS); 193 assertTrue("first note updated before it was created", lifespan > freshness); 194 assertTrue("first note visible before it was updated", freshness > exposure); 195 assertTrue("first note visibility log should have zero exposure time", exposure == 0); 196 int secondLifespan = (Integer) secondLog.peek() 197 .getTaggedData(MetricsEvent.NOTIFICATION_SINCE_CREATE_MILLIS); 198 assertTrue("first note created after second note", lifespan > secondLifespan); 199 200 mMetricsReader.checkpoint(); // clear out old logs again 201 firstLog.clear(); 202 secondLog.clear(); 203 // close the shade 204 if (mDevice.pressHome()) { 205 Thread.sleep(LONG_TIMEOUT); 206 } 207 208 MetricsAsserts.assertHasVisibilityLog("missing panel hidden log", mMetricsReader, 209 MetricsEvent.NOTIFICATION_PANEL, false); 210 firstLog = MetricsAsserts.findMatchingLogs(mMetricsReader, 211 new LogMaker(MetricsEvent.NOTIFICATION_ITEM) 212 .setType(MetricsEvent.TYPE_CLOSE) 213 .addTaggedData(MetricsEvent.NOTIFICATION_ID, firstId) 214 .setPackageName(mContext.getPackageName())); 215 assertTrue("missing first note hidden log", !firstLog.isEmpty()); 216 exposure = (Integer) firstLog.peek() 217 .getTaggedData(MetricsEvent.NOTIFICATION_SINCE_VISIBLE_MILLIS); 218 assertTrue("first note visibility log should have nonzero exposure time", exposure > 0); 219 secondLog = MetricsAsserts.findMatchingLogs(mMetricsReader, 220 new LogMaker(MetricsEvent.NOTIFICATION_ITEM) 221 .setType(MetricsEvent.TYPE_CLOSE) 222 .addTaggedData(MetricsEvent.NOTIFICATION_ID, secondId) 223 .setPackageName(mContext.getPackageName())); 224 assertTrue("missing second note hidden log", !secondLog.isEmpty()); 225 } 226 227 /** send a notification, click on first it. */ 228 @MediumTest testNotificationClicks()229 public void testNotificationClicks() throws Exception { 230 int id = CUSTOM_NOTIFICATION_ID; 231 mHelper.sendNotification(id, Notification.VISIBILITY_PUBLIC, 232 NotificationHelper.CONTENT_TITLE, true); 233 234 UiObject2 target = null; 235 if (mDevice.openNotification()) { 236 target = mDevice.wait( 237 Until.findObject(By.text(NotificationHelper.FIRST_ACTION)), 238 LONG_TIMEOUT); 239 assertNotNull("could not find first action button", target); 240 target.click(); 241 } 242 Thread.sleep(SHORT_TIMEOUT); 243 MetricsAsserts.assertHasLog("missing notification alert log", mMetricsReader, 244 new LogMaker(MetricsEvent.NOTIFICATION_ALERT) 245 .setType(MetricsEvent.TYPE_OPEN) 246 .addTaggedData(MetricsEvent.NOTIFICATION_ID, id) 247 .setSubtype(MetricsEvent.ALERT_BUZZ) // no BEEP or BLINK 248 .setPackageName(mContext.getPackageName())); 249 MetricsAsserts.assertHasLog("missing notification action 0 click log", mMetricsReader, 250 new LogMaker(MetricsEvent.NOTIFICATION_ITEM_ACTION) 251 .setType(MetricsEvent.TYPE_ACTION) 252 .addTaggedData(MetricsEvent.NOTIFICATION_ID, id) 253 .setSubtype(0) // first action button, zero indexed 254 .setPackageName(mContext.getPackageName())); 255 256 mMetricsReader.checkpoint(); // clear out old logs again 257 target = mDevice.wait(Until.findObject(By.text(NotificationHelper.SECOND_ACTION)), 258 LONG_TIMEOUT); 259 assertNotNull("could not find second action button", target); 260 target.click(); 261 Thread.sleep(SHORT_TIMEOUT); 262 MetricsAsserts.assertHasLog("missing notification action 1 click log", mMetricsReader, 263 new LogMaker(MetricsEvent.NOTIFICATION_ITEM_ACTION) 264 .setType(MetricsEvent.TYPE_ACTION) 265 .addTaggedData(MetricsEvent.NOTIFICATION_ID, id) 266 .setSubtype(1) // second action button, zero indexed 267 .setPackageName(mContext.getPackageName())); 268 269 mMetricsReader.checkpoint(); // clear out old logs again\ 270 target = mDevice.wait(Until.findObject(By.text(NotificationHelper.CONTENT_TITLE)), 271 LONG_TIMEOUT); 272 assertNotNull("could not find content click target", target); 273 target.click(); 274 Thread.sleep(SHORT_TIMEOUT); 275 MetricsAsserts.assertHasLog("missing notification content click log", mMetricsReader, 276 new LogMaker(MetricsEvent.NOTIFICATION_ITEM) 277 .setType(MetricsEvent.TYPE_ACTION) 278 .addTaggedData(MetricsEvent.NOTIFICATION_ID, id) 279 .setPackageName(mContext.getPackageName())); 280 } 281 282 @MediumTest testReceiveAndExpandRedactNotification()283 public void testReceiveAndExpandRedactNotification() throws Exception { 284 List<Integer> lists = new ArrayList<Integer>(Arrays.asList(GROUP_NOTIFICATION_ID, 285 CHILD_NOTIFICATION_ID, SECOND_CHILD_NOTIFICATION_ID)); 286 mHelper.sendBundlingNotifications(lists, BUNDLE_GROUP_KEY); 287 Thread.sleep(LONG_TIMEOUT); 288 mDevice.openNotification(); 289 UiObject2 notification = mDevice.wait( 290 Until.findObject(By.text(lists.get(1).toString())), 291 LONG_TIMEOUT * 2); 292 assertNotNull("The second notification has not been found", notification); 293 int currentY = notification.getVisibleCenter().y; 294 mDevice.wait(Until.findObject(By.res("android:id/expand_button")), LONG_TIMEOUT * 2) 295 .click(); 296 Thread.sleep(LONG_TIMEOUT); 297 notification = mDevice.wait(Until.findObject(By.text(lists.get(1).toString())), 298 LONG_TIMEOUT); 299 assertFalse("The notifications has not been bundled", 300 notification.getVisibleCenter().y == currentY); 301 mDevice.wait(Until.findObject(By.res("android:id/expand_button")), LONG_TIMEOUT).click(); 302 Thread.sleep(LONG_TIMEOUT); 303 notification = mDevice.wait(Until.findObject(By.text(lists.get(1).toString())), 304 LONG_TIMEOUT * 2); 305 assertTrue("The notifications can not be redacted", 306 notification.getVisibleCenter().y == currentY); 307 mNotificationManager.cancelAll(); 308 } 309 setAppOpsMode(String mode)310 private void setAppOpsMode(String mode) throws Exception { 311 final String command = MessageFormat.format(APPOPS_SET_SHELL_COMMAND, 312 getInstrumentation().getContext().getPackageName(), mode); 313 mDevice.executeShellCommand(command); 314 } 315 316 @MediumTest testNotificationClickedEvents()317 public void testNotificationClickedEvents() throws Exception { 318 UsageStatsManager usm = (UsageStatsManager) getInstrumentation() 319 .getContext().getSystemService(Context.USAGE_STATS_SERVICE); 320 setAppOpsMode("allow"); 321 final long startTime = System.currentTimeMillis(); 322 Context context = getInstrumentation().getContext(); 323 NotificationManager mNotificationManager = 324 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 325 int importance = NotificationManager.IMPORTANCE_DEFAULT; 326 NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID, "Channel", 327 importance); 328 // Configure the notification channel. 329 mChannel.setDescription("Test channel"); 330 mNotificationManager.createNotificationChannel(mChannel); 331 Notification.Builder mBuilder = 332 new Notification.Builder(context, CHANNEL_ID) 333 .setSmallIcon(R.drawable.stat_notify_email) 334 .setContentTitle("My notification") 335 .setContentText("Hello World!"); 336 PendingIntent pi = PendingIntent.getActivity(context, 1, 337 new Intent(Settings.ACTION_SETTINGS), 0); 338 mBuilder.setContentIntent(pi); 339 mNotificationManager.notify(1, mBuilder.build()); 340 Thread.sleep(500); 341 long endTime = System.currentTimeMillis(); 342 343 // Pull down shade 344 mDevice.openNotification(); 345 UiObject2 notification = mDevice.wait( 346 Until.findObject(By.text("My notification")),LONG_TIMEOUT * 2); 347 notification.click(); 348 boolean found = false; 349 350 outer: 351 for (int i = 0; i < 5; i++) { 352 Thread.sleep(500); 353 endTime = System.currentTimeMillis(); 354 UsageEvents events = usm.queryEvents(startTime, endTime); 355 UsageEvents.Event event = new UsageEvents.Event(); 356 while (events.hasNextEvent()) { 357 events.getNextEvent(event); 358 if (event.mEventType == Event.USER_INTERACTION) { 359 found = true; 360 break outer; 361 } 362 } 363 } 364 mDevice.pressHome(); 365 assertTrue(found); 366 } 367 findByText(String text)368 private UiObject2 findByText(String text) throws Exception { 369 int maxAttempt = 5; 370 UiObject2 item = null; 371 while (maxAttempt-- > 0) { 372 item = mDevice.wait(Until.findObject(By.text(text)), LONG_TIMEOUT); 373 if (item == null) { 374 mDevice.swipe(mDevice.getDisplayWidth() / 2, mDevice.getDisplayHeight() / 2, 375 mDevice.getDisplayWidth() / 2, 0, 30); 376 } else { 377 return item; 378 } 379 } 380 return null; 381 } 382 } 383