• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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