1 /*
<lambda>null2 * Copyright (C) 2025 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.testutils.com.android.testutils
18
19 import android.Manifest.permission.MODIFY_PHONE_STATE
20 import android.Manifest.permission.READ_PHONE_STATE
21 import android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE
22 import android.content.BroadcastReceiver
23 import android.content.Context
24 import android.content.Intent
25 import android.content.IntentFilter
26 import android.content.pm.PackageManager
27 import android.os.ConditionVariable
28 import android.os.ParcelFileDescriptor
29 import android.os.PersistableBundle
30 import android.os.Process
31 import android.telephony.CarrierConfigManager
32 import android.telephony.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED
33 import android.telephony.SubscriptionManager
34 import android.telephony.SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX
35 import android.telephony.TelephonyManager
36 import android.telephony.TelephonyManager.CarrierPrivilegesCallback
37 import android.util.Log
38 import androidx.test.platform.app.InstrumentationRegistry
39 import com.android.modules.utils.build.SdkLevel
40 import com.android.testutils.runAsShell
41 import com.android.testutils.tryTest
42 import java.security.MessageDigest
43 import kotlin.test.assertEquals
44 import kotlin.test.assertNotNull
45 import kotlin.test.assertTrue
46 import org.junit.rules.TestRule
47 import org.junit.runner.Description
48 import org.junit.runners.model.Statement
49
50 private val TAG = CarrierConfigRule::class.simpleName
51 private const val CARRIER_CONFIG_CHANGE_TIMEOUT_MS = 10_000L
52
53 /**
54 * A [TestRule] that helps set [CarrierConfigManager] overrides for tests and clean up the test
55 * configuration automatically on teardown.
56 */
57 class CarrierConfigRule : TestRule {
58 private val HEX_CHARS: CharArray = charArrayOf(
59 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
60 )
61
62 private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
63 private val uiAutomation by lazy { InstrumentationRegistry.getInstrumentation().uiAutomation }
64 private val ccm by lazy { context.getSystemService(CarrierConfigManager::class.java) }
65
66 // Map of (subId) -> (original values of overridden settings)
67 private val originalConfigs = mutableMapOf<Int, PersistableBundle>()
68
69 // Map of (subId) -> (original values of carrier service package)
70 private val originalCarrierServicePackages = mutableMapOf<Int, String?>()
71
72 override fun apply(base: Statement, description: Description): Statement {
73 return CarrierConfigStatement(base, description)
74 }
75
76 private inner class CarrierConfigStatement(
77 private val base: Statement,
78 private val description: Description
79 ) : Statement() {
80 override fun evaluate() {
81 tryTest {
82 base.evaluate()
83 } cleanup {
84 cleanUpNow()
85 }
86 }
87 }
88
89 private class ConfigChangeReceiver(private val subId: Int) : BroadcastReceiver() {
90 val cv = ConditionVariable()
91 override fun onReceive(context: Context, intent: Intent) {
92 if (intent.action != ACTION_CARRIER_CONFIG_CHANGED ||
93 intent.getIntExtra(EXTRA_SUBSCRIPTION_INDEX, -1) != subId) {
94 return
95 }
96 // This may race with other config changes for the same subId, but there is no way to
97 // know which update is being reported, and querying the override would return the
98 // latest values even before the config is applied. Config changes should be rare, so it
99 // is unlikely they would happen exactly after the override applied here and cause
100 // flakes.
101 cv.open()
102 }
103 }
104
105 private fun overrideConfigAndWait(subId: Int, config: PersistableBundle) {
106 val changeReceiver = ConfigChangeReceiver(subId)
107 context.registerReceiver(changeReceiver, IntentFilter(ACTION_CARRIER_CONFIG_CHANGED))
108 ccm.overrideConfig(subId, config)
109 assertTrue(
110 changeReceiver.cv.block(CARRIER_CONFIG_CHANGE_TIMEOUT_MS),
111 "Timed out waiting for config change for subId $subId"
112 )
113 context.unregisterReceiver(changeReceiver)
114 }
115
116 /**
117 * Add carrier config overrides with the specified configuration.
118 *
119 * The overrides will automatically be cleaned up when the test case finishes.
120 */
121 fun addConfigOverrides(subId: Int, config: PersistableBundle) {
122 val originalConfig = originalConfigs.computeIfAbsent(subId) { PersistableBundle() }
123 val overrideKeys = config.keySet()
124 val previousValues = runAsShell(READ_PHONE_STATE) {
125 ccm.getConfigForSubIdCompat(subId, overrideKeys)
126 }
127 // If a key is already in the originalConfig, keep the oldest original overrides
128 originalConfig.keySet().forEach {
129 previousValues.remove(it)
130 }
131 originalConfig.putAll(previousValues)
132
133 runAsShell(MODIFY_PHONE_STATE) {
134 overrideConfigAndWait(subId, config)
135 }
136 }
137
138 private fun runShellCommand(cmd: String) {
139 val fd: ParcelFileDescriptor = uiAutomation.executeShellCommand(cmd)
140 fd.close() // Don't care about the output.
141 }
142
143 /**
144 * Converts a byte array into a String of hexadecimal characters.
145 *
146 * @param bytes an array of bytes
147 * @return hex string representation of bytes array
148 */
149 private fun bytesToHexString(bytes: ByteArray?): String? {
150 if (bytes == null) return null
151
152 val ret = StringBuilder(2 * bytes.size)
153
154 for (i in bytes.indices) {
155 var b: Int
156 b = 0x0f and (bytes[i].toInt() shr 4)
157 ret.append(HEX_CHARS[b])
158 b = 0x0f and bytes[i].toInt()
159 ret.append(HEX_CHARS[b])
160 }
161
162 return ret.toString()
163 }
164
165 private fun setHoldCarrierPrivilege(hold: Boolean, subId: Int) {
166 if (!SdkLevel.isAtLeastT()) {
167 throw UnsupportedOperationException(
168 "Acquiring carrier privilege requires at least T SDK"
169 )
170 }
171
172 fun getCertHash(): String {
173 val pkgInfo = context.packageManager.getPackageInfo(
174 context.opPackageName,
175 PackageManager.GET_SIGNATURES
176 )
177 val digest = MessageDigest.getInstance("SHA-256")
178 val certHash = digest.digest(pkgInfo.signatures!![0]!!.toByteArray())
179 return bytesToHexString(certHash)!!
180 }
181
182 val tm = context.getSystemService(TelephonyManager::class.java)!!
183
184 val cv = ConditionVariable()
185 val cpb = PrivilegeWaiterCallback(cv)
186 // The lambda below is capturing |cpb|, whose type inherits from a class that appeared in
187 // T. This means the lambda will compile as a private method of this class taking a
188 // PrivilegeWaiterCallback argument. As JUnit uses reflection to enumerate all methods
189 // including private methods, this would fail with a link error when running on S-.
190 // To solve this, make the lambda serializable, which causes the compiler to emit a
191 // synthetic class instead of a synthetic method.
192 tryTest @JvmSerializableLambda {
193 val slotIndex = SubscriptionManager.getSlotIndex(subId)!!
194 runAsShell(READ_PRIVILEGED_PHONE_STATE) @JvmSerializableLambda {
195 tm.registerCarrierPrivilegesCallback(slotIndex, { it.run() }, cpb)
196 }
197 // Wait for the callback to be registered
198 assertTrue(cv.block(CARRIER_CONFIG_CHANGE_TIMEOUT_MS),
199 "Can't register CarrierPrivilegesCallback")
200 if (cpb.hasPrivilege == hold) {
201 if (hold) {
202 Log.w(TAG, "Package ${context.opPackageName} already is privileged")
203 } else {
204 Log.w(TAG, "Package ${context.opPackageName} already isn't privileged")
205 }
206 return@tryTest
207 }
208 cv.close()
209 if (hold) {
210 addConfigOverrides(subId, PersistableBundle().also {
211 it.putStringArray(CarrierConfigManager.KEY_CARRIER_CERTIFICATE_STRING_ARRAY,
212 arrayOf(getCertHash()))
213 })
214 } else {
215 cleanUpNow()
216 }
217 assertTrue(cv.block(CARRIER_CONFIG_CHANGE_TIMEOUT_MS),
218 "Timed out waiting for CarrierPrivilegesCallback")
219 assertEquals(cpb.hasPrivilege, hold, "Couldn't set carrier privilege")
220 } cleanup @JvmSerializableLambda {
221 runAsShell(READ_PRIVILEGED_PHONE_STATE) @JvmSerializableLambda {
222 tm.unregisterCarrierPrivilegesCallback(cpb)
223 }
224 }
225 }
226
227 /**
228 * Acquires carrier privilege on the given subscription ID.
229 */
230 fun acquireCarrierPrivilege(subId: Int) = setHoldCarrierPrivilege(true, subId)
231
232 /**
233 * Drops carrier privilege from the given subscription ID.
234 */
235 fun dropCarrierPrivilege(subId: Int) = setHoldCarrierPrivilege(false, subId)
236
237 /**
238 * Sets the carrier service package override for the given subscription ID. A null argument will
239 * clear any previously-set override.
240 */
241 fun setCarrierServicePackageOverride(subId: Int, pkg: String?) {
242 if (!SdkLevel.isAtLeastU()) {
243 throw UnsupportedOperationException(
244 "Setting carrier service package override requires at least U SDK"
245 )
246 }
247
248 val tm = context.getSystemService(TelephonyManager::class.java)!!
249
250 val cv = ConditionVariable()
251 val cpb = CarrierServiceChangedWaiterCallback(cv)
252 // The lambda below is capturing |cpb|, whose type inherits from a class that appeared in
253 // T. This means the lambda will compile as a private method of this class taking a
254 // PrivilegeWaiterCallback argument. As JUnit uses reflection to enumerate all methods
255 // including private methods, this would fail with a link error when running on S-.
256 // To solve this, make the lambda serializable, which causes the compiler to emit a
257 // synthetic class instead of a synthetic method.
258 tryTest @JvmSerializableLambda {
259 val slotIndex = SubscriptionManager.getSlotIndex(subId)!!
260 runAsShell(READ_PRIVILEGED_PHONE_STATE) @JvmSerializableLambda {
261 tm.registerCarrierPrivilegesCallback(slotIndex, { it.run() }, cpb)
262 }
263 // Wait for the callback to be registered
264 assertTrue(cv.block(CARRIER_CONFIG_CHANGE_TIMEOUT_MS),
265 "Can't register CarrierPrivilegesCallback")
266 if (cpb.pkgName == pkg) {
267 Log.w(TAG, "Carrier service package was already $pkg")
268 return@tryTest
269 }
270 if (!originalCarrierServicePackages.contains(subId)) {
271 originalCarrierServicePackages.put(subId, cpb.pkgName)
272 }
273 cv.close()
274 runAsShell(MODIFY_PHONE_STATE) {
275 if (null == pkg) {
276 // There is a bug in clear-carrier-service-package-override where not adding
277 // the -s argument will use the wrong slot index : b/299604822
278 runShellCommand("cmd phone clear-carrier-service-package-override" +
279 " -s $subId")
280 } else {
281 runShellCommand("cmd phone set-carrier-service-package-override $pkg" +
282 " -s $subId")
283 }
284 }
285 assertTrue(cv.block(CARRIER_CONFIG_CHANGE_TIMEOUT_MS),
286 "Can't modify carrier service package")
287 } cleanup @JvmSerializableLambda {
288 runAsShell(READ_PRIVILEGED_PHONE_STATE) @JvmSerializableLambda {
289 tm.unregisterCarrierPrivilegesCallback(cpb)
290 }
291 }
292 }
293
294 private class PrivilegeWaiterCallback(private val cv: ConditionVariable) :
295 CarrierPrivilegesCallback {
296 var hasPrivilege = false
297 override fun onCarrierPrivilegesChanged(p: MutableSet<String>, uids: MutableSet<Int>) {
298 hasPrivilege = uids.contains(Process.myUid())
299 cv.open()
300 }
301 }
302
303 private class CarrierServiceChangedWaiterCallback(private val cv: ConditionVariable) :
304 CarrierPrivilegesCallback {
305 var pkgName: String? = null
306 override fun onCarrierPrivilegesChanged(p: MutableSet<String>, u: MutableSet<Int>) {}
307 override fun onCarrierServiceChanged(pkgName: String?, uid: Int) {
308 this.pkgName = pkgName
309 cv.open()
310 }
311 }
312
313 /**
314 * Cleanup overrides that were added by the test case.
315 *
316 * This will be called automatically on test teardown, so it does not need to be called by the
317 * test case unless cleaning up earlier is required.
318 */
319 fun cleanUpNow() {
320 runAsShell(MODIFY_PHONE_STATE) {
321 originalConfigs.forEach { (subId, config) ->
322 try {
323 // Do not use null as the config to reset, as it would reset configs that may
324 // have been set by target preparers such as
325 // ConnectivityTestTargetPreparer / CarrierConfigSetupTest.
326 overrideConfigAndWait(subId, config)
327 } catch (e: Throwable) {
328 Log.e(TAG, "Error resetting carrier config for subId $subId")
329 }
330 }
331 originalConfigs.clear()
332 }
333 originalCarrierServicePackages.forEach { (subId, pkg) ->
334 setCarrierServicePackageOverride(subId, pkg)
335 }
336 originalCarrierServicePackages.clear()
337 }
338 }
339
CarrierConfigManagernull340 private fun CarrierConfigManager.getConfigForSubIdCompat(
341 subId: Int,
342 keys: Set<String>
343 ): PersistableBundle {
344 return if (SdkLevel.isAtLeastU()) {
345 // This method is U+
346 getConfigForSubId(subId, *keys.toTypedArray())
347 } else {
348 @Suppress("DEPRECATION")
349 val config = assertNotNull(getConfigForSubId(subId))
350 val allKeys = config.keySet().toList()
351 allKeys.forEach {
352 if (!keys.contains(it)) {
353 config.remove(it)
354 }
355 }
356 config
357 }
358 }
359