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 return runCommand("appops reset $packageName")
48 }
49
50 /**
51 * Sets the app op mode (e.g. allowed, denied) for a single package and operation.
52 */
setOpModenull53 fun setOpMode(packageName: String, opStr: String, mode: Int): String {
54 val modeStr = when (mode) {
55 MODE_ALLOWED -> "allow"
56 MODE_ERRORED -> "deny"
57 MODE_IGNORED -> "ignore"
58 MODE_DEFAULT -> "default"
59 else -> throw IllegalArgumentException("Unexpected app op type")
60 }
61 val command = "appops set $packageName $opStr $modeStr"
62 return runCommand(command)
63 }
64
65 /**
66 * Get the app op mode (e.g. MODE_ALLOWED, MODE_DEFAULT) for a single package and operation.
67 */
getOpModenull68 fun getOpMode(packageName: String, opStr: String): Int {
69 val opState = getOpState(packageName, opStr)
70 return when {
71 opState.contains(" allow") -> MODE_ALLOWED
72 opState.contains(" deny") -> MODE_ERRORED
73 opState.contains(" ignore") -> MODE_IGNORED
74 opState.contains(" default") -> MODE_DEFAULT
75 else -> throw IllegalStateException("Unexpected app op mode returned $opState")
76 }
77 }
78
79 /**
80 * Returns whether an allowed operation has been logged by the AppOpsManager for a
81 * package. Operations are noted when the app attempts to perform them and calls e.g.
82 * {@link AppOpsManager#noteOperation}.
83 *
84 * @param opStr The public string constant of the operation (e.g. OPSTR_READ_SMS).
85 */
allowedOperationLoggednull86 fun allowedOperationLogged(packageName: String, opStr: String): Boolean {
87 return getOpState(packageName, opStr).contains(" time=")
88 }
89
90 /**
91 * Returns whether a rejected operation has been logged by the AppOpsManager for a
92 * package. Operations are noted when the app attempts to perform them and calls e.g.
93 * {@link AppOpsManager#noteOperation}.
94 *
95 * @param opStr The public string constant of the operation (e.g. OPSTR_READ_SMS).
96 */
rejectedOperationLoggednull97 fun rejectedOperationLogged(packageName: String, opStr: String): Boolean {
98 return getOpState(packageName, opStr).contains(" rejectTime=")
99 }
100
101 /**
102 * Runs a lambda adopting Shell's permissions.
103 */
runWithShellPermissionIdentitynull104 inline fun <T> runWithShellPermissionIdentity(runnable: () -> T): T {
105 val uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation()
106 uiAutomation.adoptShellPermissionIdentity()
107 try {
108 return runnable.invoke()
109 } finally {
110 uiAutomation.dropShellPermissionIdentity()
111 }
112 }
113
114 /**
115 * Returns the app op state for a package. Includes information on when the operation
116 * was last attempted to be performed by the package.
117 *
118 * Format: "SEND_SMS: allow; time=+23h12m54s980ms ago; rejectTime=+1h10m23s180ms"
119 */
getOpStatenull120 private fun getOpState(packageName: String, opStr: String): String {
121 return runCommand("appops get $packageName $opStr")
122 }
123
124 /**
125 * Run a shell command.
126 *
127 * @param command Command to run
128 *
129 * @return The output of the command
130 */
runCommandnull131 fun runCommand(command: String): String {
132 return SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(), command)
133 }
134 /**
135 * Make sure that a lambda eventually finishes without throwing an exception.
136 *
137 * @param r The lambda to run.
138 * @param timeout the maximum time to wait
139 *
140 * @return the return value from the lambda
141 *
142 * @throws NullPointerException If the return value never becomes non-null
143 */
eventuallynull144 fun <T> eventually(timeout: Long = TIMEOUT_MILLIS, r: () -> T): T {
145 val start = System.currentTimeMillis()
146
147 while (true) {
148 try {
149 return r()
150 } catch (e: Throwable) {
151 val elapsed = System.currentTimeMillis() - start
152
153 if (elapsed < timeout) {
154 Log.d(LOG_TAG, "Ignoring exception", e)
155
156 Thread.sleep(minOf(100, timeout - elapsed))
157 } else {
158 throw e
159 }
160 }
161 }
162 }
163
164 /**
165 * The the {@link AppOpsManager$OpEntry} for the package name and op
166 *
167 * @param uid UID of the package
168 * @param packageName name of the package
169 * @param op name of the op
170 *
171 * @return The entry for the op
172 */
getOpEntrynull173 fun getOpEntry(uid: Int, packageName: String, op: String): OpEntry? {
174 return SystemUtil.callWithShellPermissionIdentity {
175 InstrumentationRegistry.getInstrumentation().targetContext
176 .getSystemService(AppOpsManager::class.java).getOpsForPackage(uid, packageName, op)
177 }[0].ops[0]
178 }
179
180 /**
181 * Run a block with a compat change enabled
182 */
withEnabledCompatChangenull183 fun withEnabledCompatChange(changeId: Long, packageName: String, wrapped: () -> Unit) {
184 runCommand("settings put global force_non_debuggable_final_build_for_compat 1")
185 runCommand("am compat enable $changeId $packageName")
186 try {
187 wrapped()
188 } finally {
189 runCommand("am compat reset $changeId $packageName")
190 runCommand("settings put global force_non_debuggable_final_build_for_compat 0")
191 }
192 }