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 }