• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 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 
17 package com.android.libraries.testing.deviceshadower.internal.common;
18 
19 import android.content.BroadcastReceiver;
20 import android.content.BroadcastReceiver.PendingResult;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.IntentFilter;
24 import android.os.Build.VERSION;
25 import android.os.Bundle;
26 import android.os.Handler;
27 import android.os.IBinder;
28 
29 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
30 
31 import com.android.internal.annotations.VisibleForTesting;
32 import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
33 
34 import com.google.common.base.Function;
35 import com.google.common.base.Preconditions;
36 import com.google.common.util.concurrent.AsyncFunction;
37 import com.google.common.util.concurrent.FutureCallback;
38 import com.google.common.util.concurrent.Futures;
39 import com.google.common.util.concurrent.ListenableFuture;
40 import com.google.common.util.concurrent.MoreExecutors;
41 
42 import org.robolectric.Shadows;
43 import org.robolectric.shadows.ShadowApplication;
44 import org.robolectric.util.ReflectionHelpers;
45 import org.robolectric.util.ReflectionHelpers.ClassParameter;
46 
47 import java.util.ArrayList;
48 import java.util.Collections;
49 import java.util.Comparator;
50 import java.util.HashMap;
51 import java.util.HashSet;
52 import java.util.Iterator;
53 import java.util.List;
54 import java.util.Map;
55 import java.util.Set;
56 import java.util.concurrent.atomic.AtomicBoolean;
57 
58 import javax.annotation.Nullable;
59 import javax.annotation.concurrent.GuardedBy;
60 
61 /**
62  * Manager for broadcasting of one virtual Device Shadower device.
63  *
64  * <p>Inspired by {@link ShadowApplication} and {@link LocalBroadcastManager}.
65  * <li>Broadcast permission is not supported until manifest is supported.
66  * <li>Send Broadcast is asynchronous.
67  */
68 public class BroadcastManager {
69 
70     private static final Logger LOGGER = Logger.create("BroadcastManager");
71 
72     private static final Comparator<ReceiverRecord> RECEIVER_RECORD_COMPARATOR =
73             new Comparator<ReceiverRecord>() {
74                 @Override
75                 public int compare(ReceiverRecord o1, ReceiverRecord o2) {
76                     return o2.mIntentFilter.getPriority() - o1.mIntentFilter.getPriority();
77                 }
78             };
79 
80     private final Scheduler mScheduler;
81     private final Map<String, Intent> mStickyIntents;
82 
83     @GuardedBy("mRegisteredReceivers")
84     private final Map<BroadcastReceiver, Set<String>> mRegisteredReceivers;
85 
86     @GuardedBy("mRegisteredReceivers")
87     private final Map<String, List<ReceiverRecord>> mActions;
88 
BroadcastManager(Scheduler scheduler)89     public BroadcastManager(Scheduler scheduler) {
90         this(
91                 scheduler,
92                 new HashMap<String, Intent>(),
93                 new HashMap<BroadcastReceiver, Set<String>>(),
94                 new HashMap<String, List<ReceiverRecord>>());
95     }
96 
97     @VisibleForTesting
BroadcastManager( Scheduler scheduler, Map<String, Intent> stickyIntents, Map<BroadcastReceiver, Set<String>> registeredReceivers, Map<String, List<ReceiverRecord>> actions)98     BroadcastManager(
99             Scheduler scheduler,
100             Map<String, Intent> stickyIntents,
101             Map<BroadcastReceiver, Set<String>> registeredReceivers,
102             Map<String, List<ReceiverRecord>> actions) {
103         this.mScheduler = scheduler;
104         this.mStickyIntents = stickyIntents;
105         this.mRegisteredReceivers = registeredReceivers;
106         this.mActions = actions;
107     }
108 
109     /**
110      * Registers a {@link BroadcastReceiver} with given {@link Context}.
111      *
112      * @see Context#registerReceiver(BroadcastReceiver, IntentFilter, String, Handler)
113      */
114     @Nullable
registerReceiver( @ullable BroadcastReceiver receiver, IntentFilter filter, @Nullable String broadcastPermission, @Nullable Handler handler, Context context)115     public Intent registerReceiver(
116             @Nullable BroadcastReceiver receiver,
117             IntentFilter filter,
118             @Nullable String broadcastPermission,
119             @Nullable Handler handler,
120             Context context) {
121         // Ignore broadcastPermission before fully supporting manifest
122         Preconditions.checkNotNull(filter);
123         Preconditions.checkNotNull(context);
124         if (receiver != null) {
125             synchronized (mRegisteredReceivers) {
126                 ReceiverRecord receiverRecord = new ReceiverRecord(receiver, filter, context,
127                         handler);
128                 Set<String> actionSet = mRegisteredReceivers.get(receiver);
129                 if (actionSet == null) {
130                     actionSet = new HashSet<>();
131                     mRegisteredReceivers.put(receiver, actionSet);
132                 }
133                 for (int i = 0; i < filter.countActions(); i++) {
134                     String action = filter.getAction(i);
135                     actionSet.add(action);
136                     List<ReceiverRecord> receiverRecords = mActions.get(action);
137                     if (receiverRecords == null) {
138                         receiverRecords = new ArrayList<>();
139                         mActions.put(action, receiverRecords);
140                     }
141                     receiverRecords.add(receiverRecord);
142                 }
143             }
144         }
145         return processStickyIntents(receiver, filter, context);
146     }
147 
148     // Broadcast all sticky intents matching the given IntentFilter.
149     @SuppressWarnings("FutureReturnValueIgnored")
150     @Nullable
processStickyIntents( @ullable final BroadcastReceiver receiver, IntentFilter intentFilter, final Context context)151     private Intent processStickyIntents(
152             @Nullable final BroadcastReceiver receiver,
153             IntentFilter intentFilter,
154             final Context context) {
155         Intent result = null;
156         final List<Intent> matchedIntents = new ArrayList<>();
157         for (Intent intent : mStickyIntents.values()) {
158             if (match(intentFilter, intent)) {
159                 if (result == null) {
160                     result = intent;
161                 }
162                 if (receiver == null) {
163                     return result;
164                 }
165                 matchedIntents.add(intent);
166             }
167         }
168         if (!matchedIntents.isEmpty()) {
169             mScheduler.post(
170                     NamedRunnable.create(
171                             "Broadcast.processStickyIntents",
172                             () -> {
173                                 for (Intent intent : matchedIntents) {
174                                     receiver.onReceive(context, intent);
175                                 }
176                             }));
177         }
178         return result;
179     }
180 
181     /**
182      * Unregisters a {@link BroadcastReceiver}.
183      *
184      * @see Context#unregisterReceiver(BroadcastReceiver)
185      */
unregisterReceiver(BroadcastReceiver broadcastReceiver)186     public void unregisterReceiver(BroadcastReceiver broadcastReceiver) {
187         synchronized (mRegisteredReceivers) {
188             if (!mRegisteredReceivers.containsKey(broadcastReceiver)) {
189                 LOGGER.w("Receiver not registered: " + broadcastReceiver);
190                 return;
191             }
192             Set<String> actionSet = mRegisteredReceivers.remove(broadcastReceiver);
193             for (String action : actionSet) {
194                 List<ReceiverRecord> receiverRecords = mActions.get(action);
195                 Iterator<ReceiverRecord> iterator = receiverRecords.iterator();
196                 while (iterator.hasNext()) {
197                     if (iterator.next().mBroadcastReceiver == broadcastReceiver) {
198                         iterator.remove();
199                     }
200                 }
201                 if (receiverRecords.isEmpty()) {
202                     mActions.remove(action);
203                 }
204             }
205         }
206     }
207 
208     /**
209      * Sends sticky broadcast with given {@link Intent}. This call is asynchronous.
210      *
211      * @see Context#sendStickyBroadcast(Intent)
212      */
sendStickyBroadcast(Intent intent)213     public void sendStickyBroadcast(Intent intent) {
214         mStickyIntents.put(intent.getAction(), intent);
215         sendBroadcast(intent, null /* broadcastPermission */);
216     }
217 
218     /**
219      * Sends broadcast with given {@link Intent}. Receiver permission is not supported. This call is
220      * asynchronous.
221      *
222      * @see Context#sendBroadcast(Intent, String)
223      */
224     @SuppressWarnings("FutureReturnValueIgnored")
sendBroadcast(final Intent intent, @Nullable String receiverPermission)225     public void sendBroadcast(final Intent intent, @Nullable String receiverPermission) {
226         // Ignore permission matching before fully supporting manifest
227         final List<ReceiverRecord> receivers =
228                 getMatchingReceivers(intent, false /* isOrdered */);
229         if (receivers.isEmpty()) {
230             return;
231         }
232         mScheduler.post(
233                 NamedRunnable.create(
234                         "Broadcast.sendBroadcast",
235                         () -> {
236                             for (ReceiverRecord receiverRecord : receivers) {
237                                 // Hacky: Call the shadow method, otherwise abort() NPEs after
238                                 // calling onReceive().
239                                 // TODO(b/200231384): Sending these, via context.sendBroadcast(),
240                                 //  won't NPE...but it may not be possible on each simulated
241                                 //  "device"'s main thread. Check if possible.
242                                 BroadcastReceiver broadcastReceiver =
243                                         receiverRecord.mBroadcastReceiver;
244                                 Shadows.shadowOf(broadcastReceiver)
245                                         .onReceive(receiverRecord.mContext, intent, /*abort=*/
246                                                 new AtomicBoolean(false));
247                             }
248                         }));
249     }
250 
251     /**
252      * Sends ordered broadcast with given {@link Intent}. Receiver permission is not supported. This
253      * call is asynchronous.
254      *
255      * @see Context#sendOrderedBroadcast(Intent, String)
256      */
sendOrderedBroadcast(Intent intent, @Nullable String receiverPermission)257     public void sendOrderedBroadcast(Intent intent, @Nullable String receiverPermission) {
258         sendOrderedBroadcast(
259                 intent,
260                 receiverPermission,
261                 null /* resultReceiver */,
262                 null /* handler */,
263                 0 /* initialCode */,
264                 null /* initialData */,
265                 null /* initialExtras */,
266                 null /* context */);
267     }
268 
269     /**
270      * Sends ordered broadcast with given {@link Intent} and result {@link BroadcastReceiver}.
271      * Receiver permission is not supported. This call is asynchronous.
272      *
273      * @see Context#sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String,
274      * Bundle)
275      */
276     @SuppressWarnings("FutureReturnValueIgnored")
sendOrderedBroadcast( final Intent intent, @Nullable String receiverPermission, @Nullable BroadcastReceiver resultReceiver, @Nullable Handler handler, int initialCode, @Nullable String initialData, @Nullable Bundle initialExtras, @Nullable Context context)277     public void sendOrderedBroadcast(
278             final Intent intent,
279             @Nullable String receiverPermission,
280             @Nullable BroadcastReceiver resultReceiver,
281             @Nullable Handler handler,
282             int initialCode,
283             @Nullable String initialData,
284             @Nullable Bundle initialExtras,
285             @Nullable Context context) {
286         // Ignore permission matching before fully supporting manifest
287         final List<ReceiverRecord> receivers =
288                 getMatchingReceivers(intent, true /* isOrdered */);
289         if (receivers.isEmpty()) {
290             return;
291         }
292         if (resultReceiver != null) {
293             receivers.add(
294                     new ReceiverRecord(
295                             resultReceiver, null /* intentFilter */, context, handler));
296         }
297         mScheduler.post(
298                 NamedRunnable.create(
299                         "Broadcast.sendOrderedBroadcast",
300                         () -> {
301                             postOrderedIntent(
302                                     receivers,
303                                     intent,
304                                     0 /* initialCode */,
305                                     null /* initialData */,
306                                     null /* initialExtras */);
307                         }));
308     }
309 
310     @VisibleForTesting
postOrderedIntent( List<ReceiverRecord> receivers, final Intent intent, int initialCode, @Nullable String initialData, @Nullable Bundle initialExtras)311     void postOrderedIntent(
312             List<ReceiverRecord> receivers,
313             final Intent intent,
314             int initialCode,
315             @Nullable String initialData,
316             @Nullable Bundle initialExtras) {
317         final AtomicBoolean abort = new AtomicBoolean(false);
318         ListenableFuture<BroadcastResult> resultFuture =
319                 Futures.immediateFuture(
320                         new BroadcastResult(initialCode, initialData, initialExtras));
321 
322         for (ReceiverRecord receiverRecord : receivers) {
323             final BroadcastReceiver receiver = receiverRecord.mBroadcastReceiver;
324             final Context context = receiverRecord.mContext;
325             resultFuture =
326                     Futures.transformAsync(
327                             resultFuture,
328                             new AsyncFunction<BroadcastResult, BroadcastResult>() {
329                                 @Override
330                                 public ListenableFuture<BroadcastResult> apply(
331                                         BroadcastResult input) {
332                                     PendingResult result = newPendingResult(
333                                             input.mCode, input.mData, input.mExtras,
334                                             true /* isOrdered */);
335                                     ReflectionHelpers.callInstanceMethod(
336                                             receiver, "setPendingResult",
337                                             ClassParameter.from(PendingResult.class, result));
338                                     Shadows.shadowOf(receiver).onReceive(context, intent, abort);
339                                     return BroadcastResult.transform(result);
340                                 }
341                             },
342                             MoreExecutors.directExecutor());
343         }
344         Futures.addCallback(
345                 resultFuture,
346                 new FutureCallback<BroadcastResult>() {
347                     @Override
348                     public void onSuccess(BroadcastResult result) {
349                         return;
350                     }
351 
352                     @Override
353                     public void onFailure(Throwable t) {
354                         throw new RuntimeException(t);
355                     }
356                 },
357                 MoreExecutors.directExecutor());
358     }
359 
getMatchingReceivers(Intent intent, boolean isOrdered)360     private List<ReceiverRecord> getMatchingReceivers(Intent intent, boolean isOrdered) {
361         synchronized (mRegisteredReceivers) {
362             List<ReceiverRecord> result = new ArrayList<>();
363             if (!mActions.containsKey(intent.getAction())) {
364                 return result;
365             }
366             Iterator<ReceiverRecord> iterator = mActions.get(intent.getAction()).iterator();
367             while (iterator.hasNext()) {
368                 ReceiverRecord next = iterator.next();
369                 if (match(next.mIntentFilter, intent)) {
370                     result.add(next);
371                 }
372             }
373             if (isOrdered) {
374                 Collections.sort(result, RECEIVER_RECORD_COMPARATOR);
375             }
376             return result;
377         }
378     }
379 
match(IntentFilter intentFilter, Intent intent)380     private boolean match(IntentFilter intentFilter, Intent intent) {
381         // Action test
382         if (!intentFilter.matchAction(intent.getAction())) {
383             return false;
384         }
385         // Category test
386         if (intentFilter.matchCategories(intent.getCategories()) != null) {
387             return false;
388         }
389         // Data test
390         int matchResult =
391                 intentFilter.matchData(intent.getType(), intent.getScheme(), intent.getData());
392         return matchResult != IntentFilter.NO_MATCH_TYPE
393                 && matchResult != IntentFilter.NO_MATCH_DATA;
394     }
395 
newPendingResult( int resultCode, String resultData, Bundle resultExtras, boolean isOrdered)396     private static PendingResult newPendingResult(
397             int resultCode, String resultData, Bundle resultExtras, boolean isOrdered) {
398         ClassParameter<?>[] parameters;
399         // PendingResult constructor takes different parameters in different SDK levels.
400         if (VERSION.SDK_INT < 17) {
401             parameters =
402                     ClassParameter.fromComponentLists(
403                             new Class<?>[]{
404                                     int.class,
405                                     String.class,
406                                     Bundle.class,
407                                     int.class,
408                                     boolean.class,
409                                     boolean.class,
410                                     IBinder.class
411                             },
412                             new Object[]{
413                                     resultCode,
414                                     resultData,
415                                     resultExtras,
416                                     0 /* type */,
417                                     isOrdered,
418                                     false /* sticky */,
419                                     null /* IBinder */
420                             });
421         } else if (VERSION.SDK_INT < 23) {
422             parameters =
423                     ClassParameter.fromComponentLists(
424                             new Class<?>[]{
425                                     int.class,
426                                     String.class,
427                                     Bundle.class,
428                                     int.class,
429                                     boolean.class,
430                                     boolean.class,
431                                     IBinder.class,
432                                     int.class
433                             },
434                             new Object[]{
435                                     resultCode,
436                                     resultData,
437                                     resultExtras,
438                                     0 /* type */,
439                                     isOrdered,
440                                     false /* sticky */,
441                                     null /* IBinder */,
442                                     0 /* userId */
443                             });
444         } else {
445             parameters =
446                     ClassParameter.fromComponentLists(
447                             new Class<?>[]{
448                                     int.class,
449                                     String.class,
450                                     Bundle.class,
451                                     int.class,
452                                     boolean.class,
453                                     boolean.class,
454                                     IBinder.class,
455                                     int.class,
456                                     int.class
457                             },
458                             new Object[]{
459                                     resultCode,
460                                     resultData,
461                                     resultExtras,
462                                     0 /* type */,
463                                     isOrdered,
464                                     false /* sticky */,
465                                     null /* IBinder */,
466                                     0 /* userId */,
467                                     0 /* flags */
468                             });
469         }
470         return ReflectionHelpers.callConstructor(PendingResult.class, parameters);
471     }
472 
473     /**
474      * Holder of broadcast result from previous receiver.
475      */
476     private static final class BroadcastResult {
477 
478         private final int mCode;
479         private final String mData;
480         private final Bundle mExtras;
481 
BroadcastResult(int code, String data, Bundle extras)482         BroadcastResult(int code, String data, Bundle extras) {
483             this.mCode = code;
484             this.mData = data;
485             this.mExtras = extras;
486         }
487 
transform(PendingResult result)488         private static ListenableFuture<BroadcastResult> transform(PendingResult result) {
489             return Futures.transform(
490                     Shadows.shadowOf(result).getFuture(),
491                     new Function<PendingResult, BroadcastResult>() {
492                         @Override
493                         public BroadcastResult apply(PendingResult input) {
494                             return new BroadcastResult(
495                                     input.getResultCode(), input.getResultData(),
496                                     input.getResultExtras(false));
497                         }
498                     },
499                     MoreExecutors.directExecutor());
500         }
501     }
502 
503     /**
504      * Information of a registered BroadcastReceiver.
505      */
506     @VisibleForTesting
507     static final class ReceiverRecord {
508 
509         final BroadcastReceiver mBroadcastReceiver;
510         final IntentFilter mIntentFilter;
511         final Context mContext;
512         final Handler mHandler;
513 
514         @VisibleForTesting
515         ReceiverRecord(
516                 BroadcastReceiver broadcastReceiver,
517                 IntentFilter intentFilter,
518                 Context context,
519                 Handler handler) {
520             this.mBroadcastReceiver = broadcastReceiver;
521             this.mIntentFilter = intentFilter;
522             this.mContext = context;
523             this.mHandler = handler;
524         }
525     }
526 }
527