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