1 /*
<lambda>null2 * Copyright (C) 2022 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.companiondevicetestapp
18
19 import android.Manifest.permission.ACCESS_COARSE_LOCATION
20 import android.Manifest.permission.ACCESS_FINE_LOCATION
21 import android.Manifest.permission.CALL_PHONE
22 import android.app.Activity
23 import android.bluetooth.BluetoothAdapter
24 import android.bluetooth.BluetoothDevice
25 import android.bluetooth.BluetoothManager
26 import android.bluetooth.le.ScanResult
27 import android.companion.AssociationRequest
28 import android.companion.AssociationRequest.DEVICE_PROFILE_GLASSES
29 import android.companion.AssociationRequest.DEVICE_PROFILE_WATCH
30 import android.companion.BluetoothDeviceFilter
31 import android.companion.CompanionDeviceManager
32 import android.content.ComponentName
33 import android.content.Context
34 import android.content.Intent
35 import android.content.IntentSender
36 import android.content.pm.PackageManager
37 import android.os.Bundle
38 import android.os.Handler
39 import android.os.Looper
40 import android.os.Parcelable
41 import android.os.Process
42 import android.util.Log
43 import android.widget.Button
44 import android.widget.CheckBox
45 import android.widget.EditText
46 import android.widget.LinearLayout
47 import android.widget.LinearLayout.VERTICAL
48 import android.widget.TextView
49 import android.widget.Toast
50 import java.util.regex.Pattern
51
52 class CompanionDeviceTestAppActivity : Activity() {
53
54 val associationStatus by lazy { TextView(this) }
55 val permissionStatus by lazy { TextView(this) }
56 val notificationsStatus by lazy { TextView(this) }
57 val bypassStatus by lazy { TextView(this) }
58
59 val nameFilter by lazy { EditText(this).apply {
60 hint = "Name Filter"
61 contentDescription = "name filter" // Do not change: used in the tests.
62 } }
63 val singleCheckbox by lazy { CheckBox(this).apply { text = "Single Device" } }
64 val watchCheckbox by lazy { CheckBox(this).apply { text = "Watch" } }
65 val glassesCheckbox by lazy { CheckBox(this).apply { text = "Glasses" } }
66
67 val cdm: CompanionDeviceManager by lazy { val java = CompanionDeviceManager::class.java
68 getSystemService(java)!! }
69 val bt: BluetoothAdapter by lazy { val java = BluetoothManager::class.java
70 getSystemService(java)!!.adapter }
71
72 var device: BluetoothDevice? = null
73
74 private val mainHandler = Handler(Looper.getMainLooper())
75
76 override fun onCreate(savedInstanceState: Bundle?) {
77 super.onCreate(savedInstanceState)
78
79 setContentView(LinearLayout(this).apply {
80 orientation = VERTICAL
81
82 addView(associationStatus)
83 addView(permissionStatus)
84 addView(notificationsStatus)
85 addView(bypassStatus)
86
87 addView(Button(ctx).apply {
88 text = "^^^ Refresh"
89 setOnClickListener { refresh() }
90 })
91
92 addView(nameFilter)
93 addView(singleCheckbox)
94 addView(watchCheckbox)
95 addView(glassesCheckbox)
96
97 addView(cdmButton("Associate") {
98 if (singleCheckbox.isChecked) {
99 setSingleDevice(true)
100 }
101 if (watchCheckbox.isChecked) {
102 setDeviceProfile(DEVICE_PROFILE_WATCH)
103 }
104 if (glassesCheckbox.isChecked) {
105 setDeviceProfile(DEVICE_PROFILE_GLASSES)
106 }
107 addDeviceFilter(BluetoothDeviceFilter.Builder().apply {
108 if (!nameFilter.text.isEmpty()) {
109 setNamePattern(Pattern.compile(".*${nameFilter.text}.*"))
110 }
111 }.build())
112 })
113
114 addView(Button(ctx).apply {
115 text = "Request notifications"
116 setOnClickListener {
117 cdm.requestNotificationAccess(
118 ComponentName(ctx, NotificationListener::class.java))
119 }
120 })
121 addView(Button(ctx).apply {
122 text = "Disassociate"
123 setOnClickListener {
124 cdm.associations.forEach { address ->
125 toast("Disassociating $address")
126 cdm.disassociate(address)
127 }
128 }
129 })
130
131 addView(Button(ctx).apply {
132 text = "Register PresenceListener"
133 setOnClickListener {
134 cdm.associations.forEach { address ->
135 toast("startObservingDevicePresence $address")
136 cdm.startObservingDevicePresence(address)
137 }
138 }
139 })
140
141 addView(Button(ctx).apply {
142 text = "Request permission transfer"
143 setOnClickListener {
144 cdm.myAssociations.firstNotNullOf { associationInfo ->
145 val associationId = associationInfo.id
146 toast("requestSystemDataTransfer $associationId")
147 val intentSender = cdm.buildPermissionTransferUserConsentIntent(
148 associationId)
149 if (intentSender != null) {
150 startIntentSender(intentSender, null, 0, 0, 0)
151 }
152 }
153 }
154 })
155
156 addView(Button(ctx).apply {
157 text = "Check location permission"
158 setOnClickListener {
159 val locationAccess = ctx.checkSelfPermission(ACCESS_FINE_LOCATION)
160 toast("location access: $locationAccess")
161 }
162 })
163
164 addView(Button(ctx).apply {
165 text = "Request location permission"
166 setOnClickListener {
167 requestPermissions(arrayOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION), 10)
168 }
169 })
170 })
171 }
172
173 private fun cdmButton(label: String, initReq: AssociationRequest.Builder.() -> Unit): Button {
174 return Button(ctx).apply {
175 text = label
176
177 setOnClickListener {
178 cdm.associate(AssociationRequest.Builder()
179 .apply { initReq() }
180 .build(),
181 object : CompanionDeviceManager.Callback() {
182 override fun onFailure(error: CharSequence?) {
183 toast("error: $error")
184 }
185
186 override fun onDeviceFound(chooserLauncher: IntentSender) {
187 toast("launching $chooserLauncher")
188 chooserLauncher?.let {
189 startIntentSenderForResult(it, REQUEST_CODE_CDM, null, 0, 0, 0)
190 }
191 }
192 },
193 mainHandler)
194 }
195 }
196 }
197
198 override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
199 if (requestCode == REQUEST_CODE_CDM) {
200 device = getDevice(data)
201 toast("result code: $resultCode, device: $device")
202 }
203 super.onActivityResult(requestCode, resultCode, data)
204 }
205
206 private fun getDevice(data: Intent?): BluetoothDevice? {
207 val rawDevice = data?.getParcelableExtra<Parcelable?>(CompanionDeviceManager.EXTRA_DEVICE)
208 return when (rawDevice) {
209 is BluetoothDevice -> rawDevice
210 is ScanResult -> rawDevice.device
211 else -> null
212 }
213 }
214
215 override fun onResume() {
216 super.onResume()
217 refresh()
218 }
219
220 private fun refresh() {
221 associationStatus.text = "Have associations: ${cdm.associations.isNotEmpty()}"
222
223 permissionStatus.text = "Phone granted: ${
224 checkPermission(CALL_PHONE, Process.myPid(), Process.myUid()) ==
225 PackageManager.PERMISSION_GRANTED}"
226
227 notificationsStatus.postDelayed({
228 notificationsStatus.text = "Notifications granted: ${
229 try {
230 cdm.hasNotificationAccess(
231 ComponentName.createRelative(
232 this, NotificationListener::class.java.name))
233 } catch (e: Exception) {
234 toast("" + e.message)
235 false
236 }
237 }"
238 }, 1000)
239 }
240
241 companion object {
242 const val REQUEST_CODE_CDM = 1
243 }
244 }
245
Contextnull246 fun Context.toast(msg: String) {
247 Log.i("CompanionDeviceManagerTest", "toast: $msg")
248 Toast.makeText(this, msg, Toast.LENGTH_LONG).show()
249 }
250
251 val Context.ctx get() = this
252