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 }