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