blob: a5a12a43665f1f760fe5bdd69478368f640a6596 [file] [log] [blame]
/*
* Copyright (C) 2021 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.compatibility.common.util;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Parcelable;
import android.os.SystemClock;
import android.util.Log;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Objects;
/**
* Provides a one-way communication mechanism using a Parcelable as a payload, via broadcasts.
*
* Use {@link #send(Context, String, Parcelable)} to send a message.
* Use {@link Receiver} to receive a message.
*
* Pick a unique "suffix" for your test, and use it with both the sender and receiver, in order
* to avoid "cross-talks" between different tests. (if they ever run at the same time.)
*/
public final class BroadcastMessenger {
private static final String TAG = "BroadcastMessenger";
private static final String ACTION_MESSAGE =
"com.android.compatibility.common.util.BroadcastMessenger.ACTION_MESSAGE_";
private static final String ACTION_PING =
"com.android.compatibility.common.util.BroadcastMessenger.ACTION_PING_";
private static final String EXTRA_MESSAGE =
"com.android.compatibility.common.util.BroadcastMessenger.EXTRA_MESSAGE";
/**
* We need to drop messages that were sent before the receiver was created. We keep
* track of the message send time in this extra.
*/
private static final String EXTRA_SENT_TIME =
"com.android.compatibility.common.util.BroadcastMessenger.EXTRA_SENT_TIME";
public static final int DEFAULT_TIMEOUT_MS = 10_000;
private static long getCurrentTime() {
return SystemClock.uptimeMillis();
}
private static void sendBroadcast(@NonNull Intent i, @NonNull Context context,
@NonNull String broadcastSuffix, @Nullable String receiverPackage) {
i.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
i.setPackage(receiverPackage);
i.putExtra(EXTRA_SENT_TIME, getCurrentTime());
context.sendBroadcast(i);
}
/** Send a message to the {@link Receiver} expecting a given "suffix". */
public static <T extends Parcelable> void send(@NonNull Context context,
@NonNull String broadcastSuffix, @NonNull T message) {
final Intent i = new Intent(ACTION_MESSAGE + Objects.requireNonNull(broadcastSuffix));
i.putExtra(EXTRA_MESSAGE, Objects.requireNonNull(message));
Log.i(TAG, "Sending: " + message);
sendBroadcast(i, context, broadcastSuffix, /*receiverPackage=*/ null);
}
private static void sendPing(@NonNull Context context, @NonNull String broadcastSuffix,
@NonNull String receiverPackage) {
final Intent i = new Intent(ACTION_PING + Objects.requireNonNull(broadcastSuffix));
Log.i(TAG, "Sending a ping");
sendBroadcast(i, context, broadcastSuffix, receiverPackage);
}
/**
* Receive messages sent with {@link #send}. Note it'll ignore all the messages that were
* sent before instantiated.
*
* @param <T> the class that encapsulates the message.
*/
public static final class Receiver<T extends Parcelable> implements AutoCloseable {
private final Context mContext;
private final String mBroadcastSuffix;
private final HandlerThread mReceiverThread = new HandlerThread(TAG);
private final Handler mReceiverHandler;
@GuardedBy("mMessages")
private final ArrayList<T> mMessages = new ArrayList<>();
private final long mCreatedTime = getCurrentTime();
private boolean mRegistered;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// Log.d(TAG, "Received intent: " + intent);
if (intent.getAction().equals(ACTION_MESSAGE + mBroadcastSuffix)
|| intent.getAction().equals(ACTION_PING + mBroadcastSuffix)) {
// OK
} else {
throw new RuntimeException("Unknown broadcast received: " + intent);
}
if (intent.getLongExtra(EXTRA_SENT_TIME, 0) < mCreatedTime) {
Log.i(TAG, "Dropping stale broadcast: " + intent);
return;
}
// Note for a PING, the message will be null.
final T message = intent.getParcelableExtra(EXTRA_MESSAGE);
if (message != null) {
Log.i(TAG, "Received: " + message);
}
synchronized (mMessages) {
mMessages.add(message);
mMessages.notifyAll();
}
}
};
/**
* Constructor.
*/
public Receiver(@NonNull Context context, @NonNull String broadcastSuffix) {
mContext = context;
mBroadcastSuffix = Objects.requireNonNull(broadcastSuffix);
mReceiverThread.start();
mReceiverHandler = new Handler(mReceiverThread.getLooper());
final IntentFilter fi = new IntentFilter(ACTION_MESSAGE + mBroadcastSuffix);
fi.addAction(ACTION_PING + mBroadcastSuffix);
context.registerReceiver(mReceiver, fi, /* permission=*/ null,
mReceiverHandler, Context.RECEIVER_EXPORTED);
mRegistered = true;
}
@Override
public void close() {
if (mRegistered) {
mContext.unregisterReceiver(mReceiver);
mReceiverThread.quit();
mRegistered = false;
}
}
/**
* Receive the next message with a 10 second timeout.
*/
@NonNull
public T waitForNextMessage() {
return waitForNextMessage(DEFAULT_TIMEOUT_MS);
}
/**
* Receive the next message.
*/
@NonNull
public T waitForNextMessage(long timeoutMillis) {
final T message = waitForNextMessageOrPing(timeoutMillis);
if (message == null) {
throw new RuntimeException("Received unexpected ACTION_PING");
}
return message;
}
/**
* Internal method, either return the next message, or null when a PING broadcast
* is received.
*/
@Nullable
private T waitForNextMessageOrPing(long timeoutMillis) {
final long timeout = System.currentTimeMillis() + timeoutMillis;
synchronized (mMessages) {
while (mMessages.size() == 0) {
final long wait = timeout - System.currentTimeMillis();
if (wait <= 0) {
throw new RuntimeException("Timeout waiting for the next message");
}
try {
mMessages.wait(wait);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
return mMessages.remove(0);
}
}
/**
* Ensure that no further messages have been received.
*
* Call it before {@link #close()}.
*/
public void ensureNoMoreMessages() {
// If there's a message already in mMessages, then we know it'll fail, so we don't
// need to send a ping.
// OTOH, even if there's no message enqueued, there may be broadcasts already enqueued,
// so we send a "ping" message,
synchronized (mMessages) {
if (mMessages.size() == 0) {
// Send a ping to myself.
sendPing(mContext, mBroadcastSuffix, mContext.getPackageName());
}
}
final T m = waitForNextMessageOrPing(DEFAULT_TIMEOUT_MS);
if (m == null) {
return; // Okay. Ping will deliver a null message.
}
throw new RuntimeException("No more messages expected, but received: " + m);
}
}
}