/* * 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 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 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; } 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); } } }