| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.test.notificationtrampoline; |
| |
| import android.annotation.SuppressLint; |
| import android.app.Activity; |
| import android.app.Notification; |
| import android.app.NotificationChannel; |
| import android.app.NotificationManager; |
| import android.app.PendingIntent; |
| import android.app.Service; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.os.Binder; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Message; |
| import android.os.Messenger; |
| import android.os.RemoteException; |
| import android.util.ArraySet; |
| |
| import androidx.annotation.Nullable; |
| |
| import java.lang.ref.WeakReference; |
| import java.util.Set; |
| import java.util.stream.Stream; |
| |
| /** |
| * This is a bound service used in conjunction with trampoline tests in NotificationManagerTest. |
| */ |
| public class NotificationTrampolineTestService extends Service { |
| private static final String TAG = "TrampolineTestService"; |
| private static final String NOTIFICATION_CHANNEL_ID = "cts/" + TAG; |
| private static final String EXTRA_CALLBACK = "callback"; |
| private static final String EXTRA_ACTIVITY_REF = "activity_ref"; |
| private static final String RECEIVER_ACTION = ".TRAMPOLINE"; |
| private static final int MESSAGE_BROADCAST_NOTIFICATION = 1; |
| private static final int MESSAGE_SERVICE_NOTIFICATION = 2; |
| private static final int MESSAGE_CLICK_NOTIFICATION = 3; |
| private static final int TEST_MESSAGE_BROADCAST_RECEIVED = 1; |
| private static final int TEST_MESSAGE_SERVICE_STARTED = 2; |
| private static final int TEST_MESSAGE_ACTIVITY_STARTED = 3; |
| private static final int TEST_MESSAGE_NOTIFICATION_CLICKED = 4; |
| private static final int PI_FLAGS = |
| PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; |
| |
| private final Handler mHandler = new ServiceHandler(); |
| private final ActivityReference mActivityRef = new ActivityReference(); |
| private final Set<Integer> mPostedNotifications = new ArraySet<>(); |
| private NotificationManager mNotificationManager; |
| private Messenger mMessenger; |
| private BroadcastReceiver mReceiver; |
| private Messenger mCallback; |
| private String mReceiverAction; |
| |
| @Override |
| public void onCreate() { |
| mNotificationManager = getSystemService(NotificationManager.class); |
| mMessenger = new Messenger(mHandler); |
| mReceiverAction = getPackageName() + RECEIVER_ACTION; |
| } |
| |
| @Nullable |
| @Override |
| public IBinder onBind(Intent intent) { |
| return mMessenger.getBinder(); |
| } |
| |
| @Override |
| public void onDestroy() { |
| if (mReceiver != null) { |
| unregisterReceiver(mReceiver); |
| } |
| WeakReference<Activity> activityRef = mActivityRef.activity; |
| Activity activity = (activityRef != null) ? activityRef.get() : null; |
| if (activity != null) { |
| activity.finish(); |
| } |
| for (int notificationId : mPostedNotifications) { |
| mNotificationManager.cancel(notificationId); |
| } |
| mHandler.removeCallbacksAndMessages(null); |
| } |
| |
| /** Suppressing since all messages are short-lived and we clear the queue on exit. */ |
| @SuppressLint("HandlerLeak") |
| private class ServiceHandler extends Handler { |
| @Override |
| public void handleMessage(Message message) { |
| Context context = NotificationTrampolineTestService.this; |
| mCallback = (Messenger) message.obj; |
| int notificationId = message.arg1; |
| switch (message.what) { |
| case MESSAGE_BROADCAST_NOTIFICATION: { |
| mReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent broadcastIntent) { |
| sendMessageToTest(mCallback, TEST_MESSAGE_BROADCAST_RECEIVED, true); |
| startTargetActivity(); |
| } |
| }; |
| registerReceiver(mReceiver, new IntentFilter(mReceiverAction)); |
| Intent intent = new Intent(mReceiverAction); |
| postNotification(notificationId, |
| PendingIntent.getBroadcast(context, 0, intent, PI_FLAGS)); |
| break; |
| } |
| case MESSAGE_SERVICE_NOTIFICATION: { |
| // We use this service to act as the trampoline since the bound lifecycle (which |
| // is as long as the test is being executed) outlives the started (used by the |
| // trampoline) in this case. |
| Intent intent = new Intent(context, NotificationTrampolineTestService.class); |
| postNotification(notificationId, |
| PendingIntent.getService(context, 0, intent, PI_FLAGS)); |
| break; |
| } |
| case MESSAGE_CLICK_NOTIFICATION: { |
| PendingIntent intent = Stream |
| .of(mNotificationManager.getActiveNotifications()) |
| .filter(sb -> sb.getId() == notificationId) |
| .map(sb -> sb.getNotification().contentIntent) |
| .findFirst() |
| .orElse(null); |
| if (intent != null) { |
| try { |
| intent.send(); |
| } catch (PendingIntent.CanceledException e) { |
| throw new IllegalStateException("Notification PI cancelled", e); |
| } |
| } |
| sendMessageToTest(mCallback, TEST_MESSAGE_NOTIFICATION_CLICKED, intent != null); |
| break; |
| } |
| default: |
| throw new AssertionError("Unknown message " + message.what); |
| } |
| } |
| } |
| |
| @Override |
| public int onStartCommand(Intent serviceIntent, int flags, int startId) { |
| sendMessageToTest(mCallback, TEST_MESSAGE_SERVICE_STARTED, true); |
| startTargetActivity(); |
| stopSelf(startId); |
| return START_REDELIVER_INTENT; |
| } |
| |
| private void postNotification(int notificationId, PendingIntent intent) { |
| Notification notification = |
| new Notification.Builder(this, NOTIFICATION_CHANNEL_ID) |
| .setSmallIcon(android.R.drawable.ic_info) |
| .setContentIntent(intent) |
| .build(); |
| NotificationChannel notificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, |
| NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_DEFAULT); |
| mNotificationManager.createNotificationChannel(notificationChannel); |
| mNotificationManager.notify(notificationId, notification); |
| mPostedNotifications.add(notificationId); |
| } |
| |
| private void startTargetActivity() { |
| Intent intent = new Intent(this, TargetActivity.class); |
| intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| Bundle extras = new Bundle(); |
| extras.putParcelable(EXTRA_CALLBACK, mCallback); |
| extras.putBinder(EXTRA_ACTIVITY_REF, mActivityRef); |
| intent.putExtras(extras); |
| startActivity(intent); |
| } |
| |
| private static void sendMessageToTest(Messenger callback, int message, boolean success) { |
| try { |
| callback.send(Message.obtain(null, message, success ? 0 : 1, 0)); |
| } catch (RemoteException e) { |
| throw new IllegalStateException( |
| "Couldn't send message " + message + " to test process", e); |
| } |
| } |
| |
| /** |
| * A holder object that extends from Binder just so I can send it around using startActivity() |
| * and avoid using static state. Works since the communication is local. |
| */ |
| private static class ActivityReference extends Binder { |
| public WeakReference<Activity> activity; |
| } |
| |
| public static class TargetActivity extends Activity { |
| @Override |
| protected void onResume() { |
| super.onResume(); |
| Messenger callback = getIntent().getParcelableExtra(EXTRA_CALLBACK); |
| IBinder activityRef = getIntent().getExtras().getBinder(EXTRA_ACTIVITY_REF); |
| if (activityRef instanceof ActivityReference) { |
| ((ActivityReference) activityRef).activity = new WeakReference<>(this); |
| } |
| sendMessageToTest(callback, TEST_MESSAGE_ACTIVITY_STARTED, true); |
| } |
| } |
| } |