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 androidx.glance.appwidget 18 19 import android.content.BroadcastReceiver 20 import android.content.Context 21 import android.content.Context.RECEIVER_NOT_EXPORTED 22 import android.content.Intent 23 import android.content.IntentFilter 24 import androidx.test.core.app.ApplicationProvider 25 import androidx.test.filters.MediumTest 26 import androidx.test.filters.SdkSuppress 27 import com.google.common.truth.Truth.assertThat 28 import com.google.common.truth.Truth.assertWithMessage 29 import java.time.Duration 30 import java.time.Instant 31 import java.util.concurrent.CountDownLatch 32 import java.util.concurrent.TimeUnit 33 import java.util.concurrent.atomic.AtomicReference 34 import kotlin.math.min 35 import kotlinx.coroutines.CoroutineScope 36 import kotlinx.coroutines.isActive 37 import org.junit.Test 38 39 @SdkSuppress(minSdkVersion = 26) 40 class CoroutineBroadcastReceiverTest { 41 42 private val context = ApplicationProvider.getApplicationContext<Context>() 43 44 private class TestBroadcast : BroadcastReceiver() { 45 val extraValue = AtomicReference("") 46 val broadcastExecuted = CountDownLatch(1) 47 val coroutineScopeUsed = AtomicReference<CoroutineScope>(null) 48 onReceivenull49 override fun onReceive(context: Context, intent: Intent) { 50 goAsync { 51 coroutineScopeUsed.set(this) 52 extraValue.set(intent.getStringExtra(EXTRA_STRING)) 53 broadcastExecuted.countDown() 54 // Test throwing an error directly in goAsync's job. The error should be caught, 55 // logged, and cancel the job/scope, without crashing the process. 56 error("This error should be caught and logged.") 57 } 58 } 59 } 60 61 @MediumTest 62 @Test onReceivenull63 fun onReceive() { 64 val broadcastReceiver = TestBroadcast() 65 66 if (android.os.Build.VERSION.SDK_INT < 33) { 67 context.registerReceiver( 68 broadcastReceiver, 69 IntentFilter(BROADCAST_ACTION), 70 ) 71 } else { 72 context.registerReceiver( 73 broadcastReceiver, 74 IntentFilter(BROADCAST_ACTION), 75 RECEIVER_NOT_EXPORTED, 76 ) 77 } 78 79 val value = "value" 80 context.sendBroadcast( 81 Intent(BROADCAST_ACTION) 82 .setPackage(context.packageName) 83 .putExtra(EXTRA_STRING, value) 84 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND) 85 ) 86 waitForBroadcastIdle() 87 88 assertWithMessage("Broadcast receiver did not execute") 89 .that(broadcastReceiver.broadcastExecuted.await(5, TimeUnit.SECONDS)) 90 .isTrue() 91 waitFor(Duration.ofSeconds(5)) { !broadcastReceiver.coroutineScopeUsed.get().isActive } 92 assertWithMessage("Coroutine scope did not get cancelled") 93 .that(broadcastReceiver.coroutineScopeUsed.get().isActive) 94 .isFalse() 95 assertThat(broadcastReceiver.extraValue.get()).isEqualTo(value) 96 } 97 waitFornull98 private fun waitFor(timeout: Duration, condition: () -> Boolean) { 99 val start = Instant.now() 100 val sleepMs = min(500, timeout.toMillis() / 10) 101 while (Duration.between(start, Instant.now()) < timeout) { 102 if (condition()) return 103 Thread.sleep(sleepMs) 104 } 105 } 106 107 private companion object { 108 const val BROADCAST_ACTION = "androidx.glance.appwidget.utils.TEST_ACTION" 109 const val EXTRA_STRING = "extra_string" 110 } 111 } 112