1 /* 2 * Copyright (C) 2021 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 package com.android.compatibility.common.util; 17 18 import android.content.BroadcastReceiver; 19 import android.content.Context; 20 import android.content.Intent; 21 import android.content.IntentFilter; 22 import android.os.Handler; 23 import android.os.HandlerThread; 24 import android.os.Parcelable; 25 import android.os.SystemClock; 26 import android.util.Log; 27 28 import androidx.annotation.GuardedBy; 29 import androidx.annotation.NonNull; 30 import androidx.annotation.Nullable; 31 32 import java.util.ArrayList; 33 import java.util.Objects; 34 35 /** 36 * Provides a one-way communication mechanism using a Parcelable as a payload, via broadcasts. 37 * 38 * Use {@link #send(Context, String, Parcelable)} to send a message. 39 * Use {@link Receiver} to receive a message. 40 * 41 * Pick a unique "suffix" for your test, and use it with both the sender and receiver, in order 42 * to avoid "cross-talks" between different tests. (if they ever run at the same time.) 43 */ 44 public final class BroadcastMessenger { 45 private static final String TAG = "BroadcastMessenger"; 46 47 private static final String ACTION_MESSAGE = 48 "com.android.compatibility.common.util.BroadcastMessenger.ACTION_MESSAGE_"; 49 private static final String ACTION_PING = 50 "com.android.compatibility.common.util.BroadcastMessenger.ACTION_PING_"; 51 private static final String EXTRA_MESSAGE = 52 "com.android.compatibility.common.util.BroadcastMessenger.EXTRA_MESSAGE"; 53 54 /** 55 * We need to drop messages that were sent before the receiver was created. We keep 56 * track of the message send time in this extra. 57 */ 58 private static final String EXTRA_SENT_TIME = 59 "com.android.compatibility.common.util.BroadcastMessenger.EXTRA_SENT_TIME"; 60 61 public static final int DEFAULT_TIMEOUT_MS = 10_000; 62 getCurrentTime()63 private static long getCurrentTime() { 64 return SystemClock.uptimeMillis(); 65 } 66 sendBroadcast(@onNull Intent i, @NonNull Context context, @NonNull String broadcastSuffix, @Nullable String receiverPackage)67 private static void sendBroadcast(@NonNull Intent i, @NonNull Context context, 68 @NonNull String broadcastSuffix, @Nullable String receiverPackage) { 69 i.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 70 i.setPackage(receiverPackage); 71 i.putExtra(EXTRA_SENT_TIME, getCurrentTime()); 72 73 context.sendBroadcast(i); 74 } 75 76 /** Send a message to the {@link Receiver} expecting a given "suffix". */ send(@onNull Context context, @NonNull String broadcastSuffix, @NonNull T message)77 public static <T extends Parcelable> void send(@NonNull Context context, 78 @NonNull String broadcastSuffix, @NonNull T message) { 79 final Intent i = new Intent(ACTION_MESSAGE + Objects.requireNonNull(broadcastSuffix)); 80 i.putExtra(EXTRA_MESSAGE, Objects.requireNonNull(message)); 81 82 Log.i(TAG, "Sending: " + message); 83 sendBroadcast(i, context, broadcastSuffix, /*receiverPackage=*/ null); 84 } 85 sendPing(@onNull Context context, @NonNull String broadcastSuffix, @NonNull String receiverPackage)86 private static void sendPing(@NonNull Context context, @NonNull String broadcastSuffix, 87 @NonNull String receiverPackage) { 88 final Intent i = new Intent(ACTION_PING + Objects.requireNonNull(broadcastSuffix)); 89 90 Log.i(TAG, "Sending a ping"); 91 sendBroadcast(i, context, broadcastSuffix, receiverPackage); 92 } 93 94 /** 95 * Receive messages sent with {@link #send}. Note it'll ignore all the messages that were 96 * sent before instantiated. 97 * 98 * @param <T> the class that encapsulates the message. 99 */ 100 public static final class Receiver<T extends Parcelable> implements AutoCloseable { 101 private final Context mContext; 102 private final String mBroadcastSuffix; 103 private final HandlerThread mReceiverThread = new HandlerThread(TAG); 104 private final Handler mReceiverHandler; 105 106 @GuardedBy("mMessages") 107 private final ArrayList<T> mMessages = new ArrayList<>(); 108 private final long mCreatedTime = getCurrentTime(); 109 private boolean mRegistered; 110 111 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 112 @Override 113 public void onReceive(Context context, Intent intent) { 114 // Log.d(TAG, "Received intent: " + intent); 115 if (intent.getAction().equals(ACTION_MESSAGE + mBroadcastSuffix) 116 || intent.getAction().equals(ACTION_PING + mBroadcastSuffix)) { 117 // OK 118 } else { 119 throw new RuntimeException("Unknown broadcast received: " + intent); 120 } 121 if (intent.getLongExtra(EXTRA_SENT_TIME, 0) < mCreatedTime) { 122 Log.i(TAG, "Dropping stale broadcast: " + intent); 123 return; 124 } 125 126 // Note for a PING, the message will be null. 127 final T message = intent.getParcelableExtra(EXTRA_MESSAGE); 128 if (message != null) { 129 Log.i(TAG, "Received: " + message); 130 } 131 132 synchronized (mMessages) { 133 mMessages.add(message); 134 mMessages.notifyAll(); 135 } 136 } 137 }; 138 139 /** 140 * Constructor. 141 */ Receiver(@onNull Context context, @NonNull String broadcastSuffix)142 public Receiver(@NonNull Context context, @NonNull String broadcastSuffix) { 143 mContext = context; 144 mBroadcastSuffix = Objects.requireNonNull(broadcastSuffix); 145 146 mReceiverThread.start(); 147 mReceiverHandler = new Handler(mReceiverThread.getLooper()); 148 149 final IntentFilter fi = new IntentFilter(ACTION_MESSAGE + mBroadcastSuffix); 150 fi.addAction(ACTION_PING + mBroadcastSuffix); 151 152 context.registerReceiver(mReceiver, fi, /* permission=*/ null, 153 mReceiverHandler, Context.RECEIVER_EXPORTED); 154 mRegistered = true; 155 } 156 157 @Override close()158 public void close() { 159 if (mRegistered) { 160 mContext.unregisterReceiver(mReceiver); 161 mReceiverThread.quit(); 162 mRegistered = false; 163 } 164 } 165 166 /** 167 * Receive the next message with a 10 second timeout. 168 */ 169 @NonNull waitForNextMessage()170 public T waitForNextMessage() { 171 return waitForNextMessage(DEFAULT_TIMEOUT_MS); 172 } 173 174 /** 175 * Receive the next message. 176 */ 177 @NonNull waitForNextMessage(long timeoutMillis)178 public T waitForNextMessage(long timeoutMillis) { 179 final T message = waitForNextMessageOrPing(timeoutMillis); 180 if (message == null) { 181 throw new RuntimeException("Received unexpected ACTION_PING"); 182 } 183 return message; 184 } 185 186 /** 187 * Internal method, either return the next message, or null when a PING broadcast 188 * is received. 189 */ 190 @Nullable waitForNextMessageOrPing(long timeoutMillis)191 private T waitForNextMessageOrPing(long timeoutMillis) { 192 final long timeout = System.currentTimeMillis() + timeoutMillis; 193 synchronized (mMessages) { 194 while (mMessages.size() == 0) { 195 final long wait = timeout - System.currentTimeMillis(); 196 if (wait <= 0) { 197 throw new RuntimeException("Timeout waiting for the next message"); 198 } 199 try { 200 mMessages.wait(wait); 201 } catch (InterruptedException e) { 202 throw new RuntimeException(e); 203 } 204 } 205 return mMessages.remove(0); 206 } 207 } 208 209 /** 210 * Ensure that no further messages have been received. 211 * 212 * Call it before {@link #close()}. 213 */ ensureNoMoreMessages()214 public void ensureNoMoreMessages() { 215 // If there's a message already in mMessages, then we know it'll fail, so we don't 216 // need to send a ping. 217 // OTOH, even if there's no message enqueued, there may be broadcasts already enqueued, 218 // so we send a "ping" message, 219 synchronized (mMessages) { 220 if (mMessages.size() == 0) { 221 // Send a ping to myself. 222 sendPing(mContext, mBroadcastSuffix, mContext.getPackageName()); 223 } 224 } 225 226 final T m = waitForNextMessageOrPing(DEFAULT_TIMEOUT_MS); 227 if (m == null) { 228 return; // Okay. Ping will deliver a null message. 229 } 230 throw new RuntimeException("No more messages expected, but received: " + m); 231 } 232 } 233 } 234