1 /* 2 * Copyright (C) 2022 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.safetycenter.testing 18 19 import android.os.Build.VERSION_CODES.TIRAMISU 20 import android.safetycenter.SafetyCenterData 21 import android.safetycenter.SafetyCenterEntry 22 import android.safetycenter.SafetyCenterErrorDetails 23 import android.safetycenter.SafetyCenterManager.OnSafetyCenterDataChangedListener 24 import android.safetycenter.SafetyCenterStaticEntry 25 import android.safetycenter.SafetyCenterStatus 26 import android.text.TextUtils 27 import androidx.annotation.RequiresApi 28 import androidx.test.core.app.ApplicationProvider.getApplicationContext 29 import com.android.safetycenter.testing.Coroutines.TIMEOUT_LONG 30 import com.android.safetycenter.testing.Coroutines.runBlockingWithTimeout 31 import java.time.Duration 32 import kotlinx.coroutines.channels.Channel 33 34 /** 35 * An [OnSafetyCenterDataChangedListener] that facilitates receiving updates from SafetyCenter in 36 * tests. 37 */ 38 @RequiresApi(TIRAMISU) 39 class SafetyCenterTestListener : OnSafetyCenterDataChangedListener { 40 private val dataChannel = Channel<SafetyCenterData>(Channel.UNLIMITED) 41 private val errorChannel = Channel<SafetyCenterErrorDetails>(Channel.UNLIMITED) 42 onSafetyCenterDataChangednull43 override fun onSafetyCenterDataChanged(data: SafetyCenterData) { 44 runBlockingWithTimeout { dataChannel.send(data) } 45 } 46 onErrornull47 override fun onError(errorDetails: SafetyCenterErrorDetails) { 48 // This call to super is needed for code coverage purposes, see b/272351657 for more 49 // details. The default impl of the interface is a no-op so the call to super is a no-op. 50 super.onError(errorDetails) 51 runBlockingWithTimeout { errorChannel.send(errorDetails) } 52 } 53 54 /** 55 * Waits for a [SafetyCenterData] update from SafetyCenter within the given [timeout]. 56 * 57 * Optionally, a predicate can be used to wait for the [SafetyCenterData] to be [matching]. 58 */ receiveSafetyCenterDatanull59 fun receiveSafetyCenterData( 60 timeout: Duration = TIMEOUT_LONG, 61 matching: (SafetyCenterData) -> Boolean = { true } 62 ): SafetyCenterData = <lambda>null63 runBlockingWithTimeout(timeout) { 64 var safetyCenterData = dataChannel.receive() 65 while (!matching(safetyCenterData)) { 66 safetyCenterData = dataChannel.receive() 67 } 68 safetyCenterData 69 } 70 71 /** 72 * Waits for a full Safety Center refresh to complete, where each change to the underlying 73 * [SafetyCenterData] must happen within the given [timeout]. 74 * 75 * @param withErrorEntry optionally check whether we should expect the [SafetyCenterData] to 76 * have or not have at least one an error entry after the refresh completes 77 * @return the [SafetyCenterData] after the refresh completes 78 */ waitForSafetyCenterRefreshnull79 fun waitForSafetyCenterRefresh( 80 timeout: Duration = TIMEOUT_LONG, 81 withErrorEntry: Boolean? = null 82 ): SafetyCenterData { 83 receiveSafetyCenterData(timeout) { 84 it.status.refreshStatus == SafetyCenterStatus.REFRESH_STATUS_DATA_FETCH_IN_PROGRESS || 85 it.status.refreshStatus == SafetyCenterStatus.REFRESH_STATUS_FULL_RESCAN_IN_PROGRESS 86 } 87 val afterRefresh = 88 receiveSafetyCenterData(timeout) { 89 it.status.refreshStatus == SafetyCenterStatus.REFRESH_STATUS_NONE 90 } 91 if (withErrorEntry == null) { 92 return afterRefresh 93 } 94 val errorMessage = 95 SafetyCenterTestData(getApplicationContext()) 96 .getRefreshErrorString(numberOfErrorEntries = 1) 97 val containsErrorEntry = afterRefresh.containsAnyEntryWithSummary(errorMessage) 98 if (withErrorEntry && !containsErrorEntry) { 99 throw AssertionError( 100 "No error entry with message: \"$errorMessage\" found in SafetyCenterData" + 101 " after refresh: $afterRefresh" 102 ) 103 } else if (!withErrorEntry && containsErrorEntry) { 104 throw AssertionError( 105 "Found an error entry with message: \"$errorMessage\" in SafetyCenterData" + 106 " after refresh: $afterRefresh" 107 ) 108 } 109 return afterRefresh 110 } 111 112 /** 113 * Waits for a [SafetyCenterErrorDetails] update from SafetyCenter within the given [timeout]. 114 */ receiveSafetyCenterErrorDetailsnull115 fun receiveSafetyCenterErrorDetails(timeout: Duration = TIMEOUT_LONG) = 116 runBlockingWithTimeout(timeout) { errorChannel.receive() } 117 118 /** Cancels any pending update on this [SafetyCenterTestListener]. */ cancelnull119 fun cancel() { 120 dataChannel.cancel() 121 errorChannel.cancel() 122 } 123 124 private companion object { SafetyCenterDatanull125 fun SafetyCenterData.containsAnyEntryWithSummary(summary: CharSequence): Boolean = 126 entries().any { TextUtils.equals(it.summary, summary) } || <lambda>null127 staticEntries().any { TextUtils.equals(it.summary, summary) } 128 entriesnull129 fun SafetyCenterData.entries(): List<SafetyCenterEntry> = 130 entriesOrGroups.flatMap { 131 val entry = it.entry 132 if (entry != null) { 133 listOf(entry) 134 } else { 135 it.entryGroup!!.entries 136 } 137 } 138 SafetyCenterDatanull139 fun SafetyCenterData.staticEntries(): List<SafetyCenterStaticEntry> = 140 staticEntryGroups.flatMap { it.staticEntries } 141 } 142 } 143