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.test.notificationtrampoline; 18 19 import android.annotation.SuppressLint; 20 import android.app.Activity; 21 import android.app.Notification; 22 import android.app.NotificationChannel; 23 import android.app.NotificationManager; 24 import android.app.PendingIntent; 25 import android.app.Service; 26 import android.content.BroadcastReceiver; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.IntentFilter; 30 import android.os.Binder; 31 import android.os.Bundle; 32 import android.os.Handler; 33 import android.os.IBinder; 34 import android.os.Message; 35 import android.os.Messenger; 36 import android.os.RemoteException; 37 import android.util.ArraySet; 38 39 import androidx.annotation.Nullable; 40 41 import java.lang.ref.WeakReference; 42 import java.util.Set; 43 import java.util.stream.Stream; 44 45 /** 46 * This is a bound service used in conjunction with trampoline tests in NotificationManagerTest. 47 */ 48 public class NotificationTrampolineTestService extends Service { 49 private static final String TAG = "TrampolineTestService"; 50 private static final String NOTIFICATION_CHANNEL_ID = "cts/" + TAG; 51 private static final String EXTRA_CALLBACK = "callback"; 52 private static final String EXTRA_ACTIVITY_REF = "activity_ref"; 53 private static final String RECEIVER_ACTION = ".TRAMPOLINE"; 54 private static final int MESSAGE_BROADCAST_NOTIFICATION = 1; 55 private static final int MESSAGE_SERVICE_NOTIFICATION = 2; 56 private static final int MESSAGE_CLICK_NOTIFICATION = 3; 57 private static final int MESSAGE_CANCEL_ALL_NOTIFICATIONS = 4; 58 private static final int TEST_MESSAGE_BROADCAST_RECEIVED = 1; 59 private static final int TEST_MESSAGE_SERVICE_STARTED = 2; 60 private static final int TEST_MESSAGE_ACTIVITY_STARTED = 3; 61 private static final int TEST_MESSAGE_NOTIFICATION_CLICKED = 4; 62 private static final int PI_FLAGS = 63 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; 64 private static final String GROUP_KEY = "com.android.test.notificationtrampoline.GROUP_KEY"; 65 private static final int GROUP_ID = 0; 66 67 private final Handler mHandler = new ServiceHandler(); 68 private final ActivityReference mActivityRef = new ActivityReference(); 69 private final Set<Integer> mPostedNotifications = new ArraySet<>(); 70 private NotificationManager mNotificationManager; 71 private Messenger mMessenger; 72 private BroadcastReceiver mReceiver; 73 private Messenger mCallback; 74 private String mReceiverAction; 75 private Notification mGroupSummary = null; 76 77 @Override onCreate()78 public void onCreate() { 79 mNotificationManager = getSystemService(NotificationManager.class); 80 mMessenger = new Messenger(mHandler); 81 mReceiverAction = getPackageName() + RECEIVER_ACTION; 82 } 83 84 @Nullable 85 @Override onBind(Intent intent)86 public IBinder onBind(Intent intent) { 87 return mMessenger.getBinder(); 88 } 89 90 @Override onDestroy()91 public void onDestroy() { 92 if (mReceiver != null) { 93 unregisterReceiver(mReceiver); 94 } 95 WeakReference<Activity> activityRef = mActivityRef.activity; 96 Activity activity = (activityRef != null) ? activityRef.get() : null; 97 if (activity != null) { 98 activity.finish(); 99 } 100 for (int notificationId : mPostedNotifications) { 101 mNotificationManager.cancel(notificationId); 102 } 103 mHandler.removeCallbacksAndMessages(null); 104 } 105 106 /** Suppressing since all messages are short-lived and we clear the queue on exit. */ 107 @SuppressLint("HandlerLeak") 108 private class ServiceHandler extends Handler { 109 @Override handleMessage(Message message)110 public void handleMessage(Message message) { 111 Context context = NotificationTrampolineTestService.this; 112 mCallback = (Messenger) message.obj; 113 int notificationId = message.arg1; 114 switch (message.what) { 115 case MESSAGE_BROADCAST_NOTIFICATION: { 116 mReceiver = new BroadcastReceiver() { 117 @Override 118 public void onReceive(Context context, Intent broadcastIntent) { 119 sendMessageToTest(mCallback, TEST_MESSAGE_BROADCAST_RECEIVED, true); 120 startTargetActivity(); 121 } 122 }; 123 registerReceiver(mReceiver, new IntentFilter(mReceiverAction), 124 Context.RECEIVER_EXPORTED_UNAUDITED); 125 Intent intent = new Intent(mReceiverAction); 126 postNotification(notificationId, 127 PendingIntent.getBroadcast(context, 0, intent, PI_FLAGS)); 128 break; 129 } 130 case MESSAGE_SERVICE_NOTIFICATION: { 131 // We use this service to act as the trampoline since the bound lifecycle (which 132 // is as long as the test is being executed) outlives the started (used by the 133 // trampoline) in this case. 134 Intent intent = new Intent(context, NotificationTrampolineTestService.class); 135 postNotification(notificationId, 136 PendingIntent.getService(context, 0, intent, PI_FLAGS)); 137 break; 138 } 139 case MESSAGE_CLICK_NOTIFICATION: { 140 long count = Stream.of(mNotificationManager.getActiveNotifications()) 141 .filter(sb -> sb.getId() == notificationId) 142 .flatMap(sb -> Stream.of(sb.getNotification().contentIntent, 143 sb.getNotification().publicVersion.contentIntent)) 144 .peek(intent -> { 145 try { 146 intent.send(); 147 } catch (PendingIntent.CanceledException e) { 148 throw new IllegalStateException("Notification PI cancelled", e); 149 } 150 }).count(); 151 sendMessageToTest(mCallback, TEST_MESSAGE_NOTIFICATION_CLICKED, count == 2); 152 break; 153 } 154 case MESSAGE_CANCEL_ALL_NOTIFICATIONS: { 155 cancelAllNotifications(); 156 break; 157 } 158 default: 159 throw new AssertionError("Unknown message " + message.what); 160 } 161 } 162 } 163 164 @Override onStartCommand(Intent serviceIntent, int flags, int startId)165 public int onStartCommand(Intent serviceIntent, int flags, int startId) { 166 sendMessageToTest(mCallback, TEST_MESSAGE_SERVICE_STARTED, true); 167 startTargetActivity(); 168 stopSelf(startId); 169 return START_REDELIVER_INTENT; 170 } 171 postNotification(int notificationId, PendingIntent intent)172 private void postNotification(int notificationId, PendingIntent intent) { 173 NotificationChannel notificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, 174 NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_DEFAULT); 175 mNotificationManager.createNotificationChannel(notificationChannel); 176 // If group summary hasn't been initialized, create one. All posted notifications will be 177 // added to this group. 178 if (mGroupSummary == null) { 179 mGroupSummary = new Notification.Builder(this, NOTIFICATION_CHANNEL_ID) 180 .setSmallIcon(android.R.drawable.ic_info) 181 .setContentTitle("Summary Title") 182 .setContentText("Summary Text") 183 .setGroup(GROUP_KEY) 184 .setGroupSummary(true) 185 .build(); 186 mNotificationManager.notify(GROUP_ID, mGroupSummary); 187 mPostedNotifications.add(GROUP_ID); 188 } 189 190 Notification publicNotification = 191 new Notification.Builder(this, NOTIFICATION_CHANNEL_ID) 192 .setSmallIcon(android.R.drawable.ic_info) 193 .setContentIntent(intent) 194 .build(); 195 196 Notification notification = 197 new Notification.Builder(this, NOTIFICATION_CHANNEL_ID) 198 .setSmallIcon(android.R.drawable.ic_info) 199 .setContentIntent(intent) 200 .setPublicVersion(publicNotification) 201 .setGroup(GROUP_KEY) 202 .build(); 203 204 mNotificationManager.notify(notificationId, notification); 205 mPostedNotifications.add(notificationId); 206 } 207 cancelAllNotifications()208 private void cancelAllNotifications() { 209 mNotificationManager.cancelAll(); 210 mPostedNotifications.clear(); 211 } 212 startTargetActivity()213 private void startTargetActivity() { 214 Intent intent = new Intent(this, TargetActivity.class); 215 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 216 Bundle extras = new Bundle(); 217 extras.putParcelable(EXTRA_CALLBACK, mCallback); 218 extras.putBinder(EXTRA_ACTIVITY_REF, mActivityRef); 219 intent.putExtras(extras); 220 startActivity(intent); 221 } 222 sendMessageToTest(Messenger callback, int message, boolean success)223 private static void sendMessageToTest(Messenger callback, int message, boolean success) { 224 try { 225 callback.send(Message.obtain(null, message, success ? 0 : 1, 0)); 226 } catch (RemoteException e) { 227 throw new IllegalStateException( 228 "Couldn't send message " + message + " to test process", e); 229 } 230 } 231 232 /** 233 * A holder object that extends from Binder just so I can send it around using startActivity() 234 * and avoid using static state. Works since the communication is local. 235 */ 236 private static class ActivityReference extends Binder { 237 public WeakReference<Activity> activity; 238 } 239 240 public static class TargetActivity extends Activity { 241 @Override onResume()242 protected void onResume() { 243 super.onResume(); 244 Messenger callback = getIntent().getParcelableExtra(EXTRA_CALLBACK); 245 IBinder activityRef = getIntent().getExtras().getBinder(EXTRA_ACTIVITY_REF); 246 if (activityRef instanceof ActivityReference) { 247 ((ActivityReference) activityRef).activity = new WeakReference<>(this); 248 } 249 sendMessageToTest(callback, TEST_MESSAGE_ACTIVITY_STARTED, true); 250 } 251 } 252 } 253