1 /*
<lambda>null2 * Copyright (C) 2019 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 android.os.cts
18
19 import android.companion.CompanionDeviceManager
20 import android.content.pm.PackageManager.FEATURE_AUTOMOTIVE
21 import android.content.pm.PackageManager.FEATURE_COMPANION_DEVICE_SETUP
22 import android.content.pm.PackageManager.PERMISSION_GRANTED
23 import android.net.MacAddress
24 import android.os.Binder
25 import android.os.Bundle
26 import android.os.Parcelable
27 import android.os.UserHandle
28 import android.platform.test.annotations.AppModeFull
29 import android.test.InstrumentationTestCase
30 import android.util.Size
31 import android.util.SizeF
32 import android.util.SparseArray
33 import android.view.accessibility.AccessibilityNodeInfo
34 import android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE
35 import android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT
36 import android.widget.EditText
37 import android.widget.TextView
38 import androidx.test.InstrumentationRegistry
39 import androidx.test.runner.AndroidJUnit4
40 import com.android.compatibility.common.util.MatcherUtils
41 import com.android.compatibility.common.util.MatcherUtils.hasIdThat
42 import com.android.compatibility.common.util.SystemUtil.eventually
43 import com.android.compatibility.common.util.SystemUtil.getEventually
44 import com.android.compatibility.common.util.SystemUtil.runShellCommand
45 import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
46 import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
47 import com.android.compatibility.common.util.ThrowingSupplier
48 import com.android.compatibility.common.util.UiAutomatorUtils.waitFindObject
49 import com.android.compatibility.common.util.children
50 import com.android.compatibility.common.util.click
51 import org.hamcrest.CoreMatchers.`is`
52 import org.hamcrest.CoreMatchers.containsString
53 import org.hamcrest.CoreMatchers.equalTo
54 import org.hamcrest.Matcher
55 import org.hamcrest.Matchers.empty
56 import org.hamcrest.Matchers.not
57 import org.junit.Assert.assertThat
58 import org.junit.Assume.assumeFalse
59 import org.junit.Assume.assumeTrue
60 import org.junit.Before
61 import org.junit.After
62 import org.junit.Test
63 import org.junit.runner.RunWith
64 import java.io.Serializable
65
66 const val COMPANION_APPROVE_WIFI_CONNECTIONS =
67 "android.permission.COMPANION_APPROVE_WIFI_CONNECTIONS"
68 const val DUMMY_MAC_ADDRESS = "00:00:00:00:00:10"
69 const val MANAGE_COMPANION_DEVICES = "android.permission.MANAGE_COMPANION_DEVICES"
70 const val SHELL_PACKAGE_NAME = "com.android.shell"
71 val InstrumentationTestCase.context get() = InstrumentationRegistry.getTargetContext()
72
73 /**
74 * Test for [CompanionDeviceManager]
75 */
76 @RunWith(AndroidJUnit4::class)
77 class CompanionDeviceManagerTest : InstrumentationTestCase() {
78
79 val cdm: CompanionDeviceManager by lazy {
80 context.getSystemService(CompanionDeviceManager::class.java)
81 }
82
83 private fun isShellAssociated(macAddress: String, packageName: String): Boolean {
84 val userId = context.userId
85 return runShellCommand("cmd companiondevice list $userId")
86 .lines()
87 .any {
88 packageName in it && macAddress in it
89 }
90 }
91
92 private fun isCdmAssociated(
93 macAddress: String,
94 packageName: String,
95 vararg permissions: String
96 ): Boolean {
97 return runWithShellPermissionIdentity(ThrowingSupplier {
98 cdm.isDeviceAssociatedForWifiConnection(packageName,
99 MacAddress.fromString(macAddress), context.user)
100 }, *permissions)
101 }
102
103 @Before
104 fun assumeHasFeature() {
105 assumeTrue(context.packageManager.hasSystemFeature(FEATURE_COMPANION_DEVICE_SETUP))
106 // TODO(b/191699828) test does not work in automotive due to accessibility issue
107 assumeFalse(context.packageManager.hasSystemFeature(FEATURE_AUTOMOTIVE))
108 }
109
110 @After
111 fun removeAllAssociations() {
112 val packageName = "android.os.cts.companiontestapp"
113 val userId = context.userId
114 val associations = getAssociatedDevices(packageName)
115
116 for (address in associations) {
117 runShellCommandOrThrow("cmd companiondevice disassociate $userId $packageName $address")
118 }
119 }
120
121 @AppModeFull(reason = "Companion API for non-instant apps only")
122 @Test
123 fun testIsDeviceAssociated() {
124 val userId = context.userId
125 val packageName = context.packageName
126
127 assertFalse(isCdmAssociated(DUMMY_MAC_ADDRESS, packageName, MANAGE_COMPANION_DEVICES))
128 assertFalse(isShellAssociated(DUMMY_MAC_ADDRESS, packageName))
129
130 try {
131 runShellCommand(
132 "cmd companiondevice associate $userId $packageName $DUMMY_MAC_ADDRESS")
133 assertTrue(isCdmAssociated(DUMMY_MAC_ADDRESS, packageName, MANAGE_COMPANION_DEVICES))
134 assertTrue(isShellAssociated(DUMMY_MAC_ADDRESS, packageName))
135 } finally {
136 runShellCommand(
137 "cmd companiondevice disassociate $userId $packageName $DUMMY_MAC_ADDRESS")
138 }
139 }
140
141 @AppModeFull(reason = "Companion API for non-instant apps only")
142 @Test
143 fun testIsDeviceAssociatedWithCompanionApproveWifiConnectionsPermission() {
144 assertTrue(isCdmAssociated(
145 DUMMY_MAC_ADDRESS, SHELL_PACKAGE_NAME, MANAGE_COMPANION_DEVICES,
146 COMPANION_APPROVE_WIFI_CONNECTIONS))
147 assertFalse(isShellAssociated(DUMMY_MAC_ADDRESS, SHELL_PACKAGE_NAME))
148 }
149
150 @AppModeFull(reason = "Companion API for non-instant apps only")
151 @Test
152 fun testDump() {
153 val userId = context.userId
154 val packageName = context.packageName
155
156 try {
157 runShellCommand(
158 "cmd companiondevice associate $userId $packageName $DUMMY_MAC_ADDRESS")
159 val output = runShellCommand("dumpsys companiondevice")
160 assertThat(output, containsString(packageName))
161 assertThat(output, containsString(DUMMY_MAC_ADDRESS))
162 } finally {
163 runShellCommand(
164 "cmd companiondevice disassociate $userId $packageName $DUMMY_MAC_ADDRESS")
165 }
166 }
167
168 @AppModeFull(reason = "Companion API for non-instant apps only")
169 @Test
170 fun testProfiles() {
171 val packageName = "android.os.cts.companiontestapp"
172 installApk(
173 "--user ${UserHandle.myUserId()} /data/local/tmp/cts/os/CtsCompanionTestApp.apk")
174 startApp(packageName)
175
176 waitFindNode(hasClassThat(`is`(equalTo(EditText::class.java.name))))
177 .performAction(ACTION_SET_TEXT,
178 bundleOf(ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE to ""))
179 waitForIdle()
180
181 click("Watch")
182 val device = getEventually({
183 click("Associate")
184 waitFindNode(hasIdThat(containsString("device_list")),
185 failMsg = "Test requires a discoverable bluetooth device nearby",
186 timeoutMs = 9_000)
187 .children
188 .find { it.className == TextView::class.java.name }
189 .assertNotNull { "Empty device list" }
190 }, 90_000)
191 device!!.click()
192
193 eventually {
194 assertThat(getAssociatedDevices(packageName), not(empty()))
195 }
196 val deviceAddress = getAssociatedDevices(packageName).last()
197
198 runShellCommandOrThrow("cmd companiondevice simulate_connect $deviceAddress")
199 assertPermission(packageName, "android.permission.CALL_PHONE", PERMISSION_GRANTED)
200
201 runShellCommandOrThrow("cmd companiondevice simulate_disconnect $deviceAddress")
202 assertPermission(packageName, "android.permission.CALL_PHONE", PERMISSION_GRANTED)
203 }
204
205 @AppModeFull(reason = "Companion API for non-instant apps only")
206 @Test
207 fun testRequestNotifications() {
208 val packageName = "android.os.cts.companiontestapp"
209 installApk(
210 "--user ${UserHandle.myUserId()} /data/local/tmp/cts/os/CtsCompanionTestApp.apk")
211 startApp(packageName)
212
213 waitFindNode(hasClassThat(`is`(equalTo(EditText::class.java.name))))
214 .performAction(ACTION_SET_TEXT,
215 bundleOf(ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE to ""))
216 waitForIdle()
217
218 val deviceForAssociation = getEventually({
219 click("Associate")
220 waitFindNode(hasIdThat(containsString("device_list")),
221 failMsg = "Test requires a discoverable bluetooth device nearby",
222 timeoutMs = 5_000)
223 .children
224 .find { it.className == TextView::class.java.name }
225 .assertNotNull { "Empty device list" }
226 }, 60_000)
227
228 deviceForAssociation!!.click()
229
230 waitForIdle()
231
232 val deviceForNotifications = getEventually({
233 click("Request Notifications")
234 waitFindNode(hasIdThat(containsString("button1")),
235 failMsg = "The Request Notifications dialog is not showing up",
236 timeoutMs = 5_000)
237 .assertNotNull { "Request Notifications is not implemented" }
238 }, 60_000)
239
240 deviceForNotifications!!.click()
241
242 waitForIdle()
243 }
244
245 private fun getAssociatedDevices(
246 pkg: String,
247 user: UserHandle = android.os.Process.myUserHandle()
248 ): List<String> {
249 return runShellCommandOrThrow("cmd companiondevice list ${user.identifier}")
250 .lines()
251 .filter { it.startsWith(pkg) }
252 .map { it.substringAfterLast(" ") }
253 }
254 }
255
clicknull256 private fun click(label: String) {
257 waitFindObject(byTextIgnoreCase(label)).click()
258 waitForIdle()
259 }
260
hasClassThatnull261 fun hasClassThat(condition: Matcher<in String?>?): Matcher<AccessibilityNodeInfo> {
262 return MatcherUtils.propertyMatches(
263 "class",
264 { obj: AccessibilityNodeInfo -> obj.className },
265 condition)
266 }
267
<lambda>null268 fun bundleOf(vararg entries: Pair<String, Any>) = Bundle().apply {
269 entries.forEach { (k, v) -> set(k, v) }
270 }
271
setnull272 operator fun Bundle.set(key: String, value: Any?) {
273 if (value is Array<*> && value.isArrayOf<Parcelable>()) {
274 putParcelableArray(key, value as Array<Parcelable>)
275 return
276 }
277 if (value is Array<*> && value.isArrayOf<CharSequence>()) {
278 putCharSequenceArray(key, value as Array<CharSequence>)
279 return
280 }
281 when (value) {
282 is Byte -> putByte(key, value)
283 is Char -> putChar(key, value)
284 is Short -> putShort(key, value)
285 is Float -> putFloat(key, value)
286 is CharSequence -> putCharSequence(key, value)
287 is Parcelable -> putParcelable(key, value)
288 is Size -> putSize(key, value)
289 is SizeF -> putSizeF(key, value)
290 is ArrayList<*> -> putParcelableArrayList(key, value as ArrayList<Parcelable>)
291 is SparseArray<*> -> putSparseParcelableArray(key, value as SparseArray<Parcelable>)
292 is Serializable -> putSerializable(key, value)
293 is ByteArray -> putByteArray(key, value)
294 is ShortArray -> putShortArray(key, value)
295 is CharArray -> putCharArray(key, value)
296 is FloatArray -> putFloatArray(key, value)
297 is Bundle -> putBundle(key, value)
298 is Binder -> putBinder(key, value)
299 else -> throw IllegalArgumentException("" + value)
300 }
301 }