xref: /aosp_15_r20/cts/tests/tests/appop/appopsTestUtilLib/src/android/app/appops/cts/AppOpsUtils.kt (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1 /*
2  * Copyright (C) 2018 Google Inc.
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 android.app.appops.cts
18 
19 import android.app.AppOpsManager
20 import android.app.AppOpsManager.MODE_ALLOWED
21 import android.app.AppOpsManager.MODE_DEFAULT
22 import android.app.AppOpsManager.MODE_ERRORED
23 import android.app.AppOpsManager.MODE_IGNORED
24 import android.app.AppOpsManager.OpEntry
25 import android.util.Log
26 import androidx.test.InstrumentationRegistry
27 import com.android.compatibility.common.util.SystemUtil
28 
29 private const val LOG_TAG = "AppOpsUtils"
30 private const val TIMEOUT_MILLIS = 10000L
31 
32 const val TEST_ATTRIBUTION_TAG = "testAttribution"
33 
34 /**
35  * Resets a package's app ops configuration to the device default. See AppOpsManager for the
36  * default op settings.
37  *
38  * <p>
39  * It's recommended to call this in setUp() and tearDown() of your test so the test starts and
40  * ends with a reproducible default state, and so doesn't affect other tests.
41  *
42  * <p>
43  * Some app ops are configured to be non-resettable, which means that the state of these will
44  * not be reset even when calling this method.
45  */
resetnull46 fun reset(packageName: String): String {
47     val userId = InstrumentationRegistry.getInstrumentation().targetContext.userId
48     return runCommand("appops reset --user $userId $packageName")
49 }
50 
51 /**
52  * Sets the app op mode (e.g. allowed, denied) for a single package and operation.
53  */
setOpModenull54 fun setOpMode(packageName: String, opStr: String, mode: Int): String {
55     val modeStr = when (mode) {
56         MODE_ALLOWED -> "allow"
57         MODE_ERRORED -> "deny"
58         MODE_IGNORED -> "ignore"
59         MODE_DEFAULT -> "default"
60         else -> throw IllegalArgumentException("Unexpected app op type")
61     }
62     val userId = InstrumentationRegistry.getInstrumentation().targetContext.userId
63     val command = "appops set --user $userId $packageName $opStr $modeStr"
64     return runCommand(command)
65 }
66 
67 /**
68  * Get the app op mode (e.g. MODE_ALLOWED, MODE_DEFAULT) for a single package and operation.
69  */
getOpModenull70 fun getOpMode(packageName: String, opStr: String): Int {
71     val opState = getOpState(packageName, opStr)
72     return when {
73         opState.contains(" allow") -> MODE_ALLOWED
74         opState.contains(" deny") -> MODE_ERRORED
75         opState.contains(" ignore") -> MODE_IGNORED
76         opState.contains(" default") -> MODE_DEFAULT
77         else -> throw IllegalStateException("Unexpected app op mode returned $opState")
78     }
79 }
80 
81 /**
82  * Returns whether an allowed operation has been logged by the AppOpsManager for a
83  * package. Operations are noted when the app attempts to perform them and calls e.g.
84  * {@link AppOpsManager#noteOperation}.
85  *
86  * @param opStr The public string constant of the operation (e.g. OPSTR_READ_SMS).
87  */
allowedOperationLoggednull88 fun allowedOperationLogged(packageName: String, opStr: String): Boolean {
89     return getOpState(packageName, opStr).contains(" time=")
90 }
91 
92 /**
93  * Returns whether a rejected operation has been logged by the AppOpsManager for a
94  * package. Operations are noted when the app attempts to perform them and calls e.g.
95  * {@link AppOpsManager#noteOperation}.
96  *
97  * @param opStr The public string constant of the operation (e.g. OPSTR_READ_SMS).
98  */
rejectedOperationLoggednull99 fun rejectedOperationLogged(packageName: String, opStr: String): Boolean {
100     return getOpState(packageName, opStr).contains(" rejectTime=")
101 }
102 
103 /**
104  * Runs a lambda adopting Shell's permissions.
105  */
runWithShellPermissionIdentitynull106 inline fun <T> runWithShellPermissionIdentity(runnable: () -> T): T {
107     val uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation()
108     uiAutomation.adoptShellPermissionIdentity()
109     try {
110         return runnable.invoke()
111     } finally {
112         uiAutomation.dropShellPermissionIdentity()
113     }
114 }
115 
116 /**
117  * Returns the app op state for a package. Includes information on when the operation
118  * was last attempted to be performed by the package.
119  *
120  * Format: "SEND_SMS: allow; time=+23h12m54s980ms ago; rejectTime=+1h10m23s180ms"
121  */
getOpStatenull122 private fun getOpState(packageName: String, opStr: String): String {
123     val userId = InstrumentationRegistry.getInstrumentation().targetContext.userId
124     return runCommand("appops get --user $userId $packageName $opStr")
125 }
126 
127 /**
128  * Run a shell command.
129  *
130  * @param command Command to run
131  *
132  * @return The output of the command
133  */
runCommandnull134 fun runCommand(command: String): String {
135     return SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(), command)
136 }
137 /**
138  * Make sure that a lambda eventually finishes without throwing an exception.
139  *
140  * @param r The lambda to run.
141  * @param timeout the maximum time to wait
142  *
143  * @return the return value from the lambda
144  *
145  * @throws NullPointerException If the return value never becomes non-null
146  */
eventuallynull147 fun <T> eventually(timeout: Long = TIMEOUT_MILLIS, r: () -> T): T {
148     val start = System.currentTimeMillis()
149 
150     while (true) {
151         try {
152             return r()
153         } catch (e: Throwable) {
154             val elapsed = System.currentTimeMillis() - start
155 
156             if (elapsed < timeout) {
157                 Log.d(LOG_TAG, "Ignoring exception", e)
158 
159                 Thread.sleep(minOf(100, timeout - elapsed))
160             } else {
161                 throw e
162             }
163         }
164     }
165 }
166 
167 /**
168  * The the {@link AppOpsManager$OpEntry} for the package name and op
169  *
170  * @param uid UID of the package
171  * @param packageName name of the package
172  * @param op name of the op
173  *
174  * @return The entry for the op
175  */
getOpEntrynull176 fun getOpEntry(uid: Int, packageName: String, op: String): OpEntry? {
177     return SystemUtil.callWithShellPermissionIdentity {
178         InstrumentationRegistry.getInstrumentation().targetContext
179                 .getSystemService(AppOpsManager::class.java).getOpsForPackage(uid, packageName, op)
180     }[0].ops[0]
181 }
182 
183 /**
184  * Run a block with a compat change enabled
185  */
withEnabledCompatChangenull186 fun withEnabledCompatChange(changeId: Long, packageName: String, wrapped: () -> Unit) {
187     runCommand("settings put global force_non_debuggable_final_build_for_compat 1")
188     runCommand("am compat enable $changeId $packageName")
189     try {
190         wrapped()
191     } finally {
192         runCommand("am compat reset $changeId $packageName")
193         runCommand("settings put global force_non_debuggable_final_build_for_compat 0")
194     }
195 }