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 }