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