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