1 /* 2 * 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.companiontestapp; 18 19 import android.Manifest; 20 import android.app.Activity; 21 import android.bluetooth.BluetoothAdapter; 22 import android.bluetooth.BluetoothDevice; 23 import android.bluetooth.BluetoothManager; 24 import android.bluetooth.BluetoothServerSocket; 25 import android.bluetooth.BluetoothSocket; 26 import android.companion.AssociationInfo; 27 import android.companion.AssociationRequest; 28 import android.companion.CompanionDeviceManager; 29 import android.companion.CompanionException; 30 import android.companion.Flags; 31 import android.companion.cts.permissionssynctestapp.R; 32 import android.content.ComponentName; 33 import android.content.Context; 34 import android.content.Intent; 35 import android.content.IntentSender; 36 import android.content.ServiceConnection; 37 import android.content.pm.PackageManager; 38 import android.os.Bundle; 39 import android.os.IBinder; 40 import android.os.OutcomeReceiver; 41 import android.util.Log; 42 import android.widget.Button; 43 import android.widget.TextView; 44 import android.widget.Toast; 45 46 import androidx.annotation.NonNull; 47 import androidx.annotation.Nullable; 48 49 import java.io.IOException; 50 import java.util.Comparator; 51 import java.util.List; 52 import java.util.UUID; 53 54 public class MainActivity extends Activity { 55 static final String TAG = "CDM_PermissionsSyncTestApp"; 56 57 // Name for the SDP record when creating server socket 58 private static final String SERVICE_NAME = "CDMPermissionsSyncBluetoothSecure"; 59 60 // Unique UUID for this application 61 private static final UUID SERVICE_UUID = 62 UUID.fromString("7606c653-6dc3-4a61-9870-07652896cc1c"); 63 64 private BluetoothAdapter mAdapter; 65 private BluetoothServerThread mServerThread; 66 private BluetoothDevice mClientDevice; 67 68 private CompanionDeviceManager mCompanionDeviceManager; 69 private AssociationInfo mAssociationInfo; 70 71 private TextView mAssociationDisplay; 72 private Button mAssociateButton; 73 private Button mDisassociateButton; 74 private Button mPermissionsSyncButton; 75 private Button mDiscoverableButton; 76 77 private static final int REQUEST_CODE_DISCOVERABLE = 100; 78 private static final int REQUEST_CODE_ASSOCIATE = 101; 79 private static final int REQUEST_CODE_SYNC = 102; 80 private static final int REQUEST_CODE_BLUETOOTH_CONNECT_PERMISSION = 103; 81 82 @Override onCreate(Bundle savedInstanceState)83 protected void onCreate(Bundle savedInstanceState) { 84 super.onCreate(savedInstanceState); 85 setContentView(R.layout.main_activity); 86 87 mAdapter = getSystemService(BluetoothManager.class).getAdapter(); 88 89 mCompanionDeviceManager = getSystemService(CompanionDeviceManager.class); 90 91 mAssociationDisplay = requireViewById(R.id.associatedDeviceInfo); 92 93 mAssociateButton = requireViewById(R.id.associateButton); 94 mAssociateButton.setOnClickListener(v -> mCompanionDeviceManager.associate( 95 new AssociationRequest.Builder() 96 .setDisplayName("Test Device") 97 .build(), 98 getMainExecutor(), mCallback)); 99 100 mDisassociateButton = requireViewById(R.id.disassociateButton); 101 mDisassociateButton.setOnClickListener(v -> { 102 mCompanionDeviceManager.disassociate(mAssociationInfo.getId()); 103 updateAssociationInfo(null); 104 }); 105 106 mPermissionsSyncButton = requireViewById(R.id.beginPermissionsSyncButton); 107 mPermissionsSyncButton.setOnClickListener(v -> { 108 if (Flags.permSyncUserConsent() && mCompanionDeviceManager 109 .isPermissionTransferUserConsented(mAssociationInfo.getId())) { 110 startPermissionsSync(); 111 } else { 112 requestPermissionsSyncUserConsent(); 113 } 114 }); 115 116 mDiscoverableButton = requireViewById(R.id.startAdvertisingButton); 117 mDiscoverableButton.setOnClickListener(v -> startActivityForResult( 118 new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE), 119 REQUEST_CODE_DISCOVERABLE)); 120 121 List<AssociationInfo> myAssociations = mCompanionDeviceManager.getMyAssociations(); 122 AssociationInfo myAssociation = myAssociations.stream() 123 .max(Comparator.comparingInt(AssociationInfo::getId)) 124 .orElse(null); 125 updateAssociationInfo(myAssociation); 126 } 127 128 @Override onStart()129 protected void onStart() { 130 super.onStart(); 131 132 bindService(new Intent(this, PermissionsTransferCompanionService.class), 133 mServiceConnection, Context.BIND_AUTO_CREATE); 134 135 mServerThread = new BluetoothServerThread(); 136 137 // Start the thread right away if permission already granted; else request permission and 138 // start the thread in onActivityResult 139 if (requestBluetoothPermissionIfNeeded()) { 140 mServerThread.start(); 141 } 142 } 143 144 @Override onStop()145 protected void onStop() { 146 super.onStop(); 147 148 mServerThread.shutdown(); 149 mServerThread = null; 150 151 unbindService(mServiceConnection); 152 } 153 updateAssociationInfo(AssociationInfo association)154 private void updateAssociationInfo(AssociationInfo association) { 155 mAssociationInfo = association; 156 mAssociateButton.setEnabled(mAssociationInfo == null); 157 mDisassociateButton.setEnabled(mAssociationInfo != null); 158 mPermissionsSyncButton.setEnabled(mAssociationInfo != null); 159 if (mAssociationInfo == null) { 160 mAssociationDisplay.setText("Not associated."); 161 mClientDevice = null; 162 } else { 163 String text = "association id=" + mAssociationInfo.getId() 164 + "\ndevice address=" + mAssociationInfo.getDeviceMacAddress(); 165 if (Flags.permSyncUserConsent()) { 166 text = text + "\nuser consented=" + mCompanionDeviceManager 167 .isPermissionTransferUserConsented(mAssociationInfo.getId()); 168 } 169 mAssociationDisplay.setText(text); 170 mClientDevice = mAdapter.getRemoteDevice( 171 mAssociationInfo.getDeviceMacAddress().toString().toUpperCase()); 172 } 173 } 174 requestBluetoothPermissionIfNeeded()175 private boolean requestBluetoothPermissionIfNeeded() { 176 if (checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) 177 == PackageManager.PERMISSION_DENIED) { 178 requestPermissions(new String[]{ Manifest.permission.BLUETOOTH_CONNECT }, 179 REQUEST_CODE_BLUETOOTH_CONNECT_PERMISSION); 180 return false; 181 } 182 return true; 183 } 184 requestPermissionsSyncUserConsent()185 private void requestPermissionsSyncUserConsent() { 186 try { 187 final IntentSender intentSender = mCompanionDeviceManager 188 .buildPermissionTransferUserConsentIntent(mAssociationInfo.getId()); 189 startIntentSenderForResult(intentSender, REQUEST_CODE_SYNC, null, 0, 0, 0); 190 } catch (IntentSender.SendIntentException e) { 191 throw new RuntimeException(e); 192 } 193 } 194 startPermissionsSync()195 private void startPermissionsSync() { 196 try { 197 final BluetoothSocket socket = mClientDevice 198 .createRfcommSocketToServiceRecord(SERVICE_UUID); 199 socket.connect(); 200 Log.v(TAG, "Attaching client socket " + socket); 201 PermissionsTransferCompanionService.sInstance.attachSystemDataTransport( 202 mAssociationInfo.getId(), 203 socket.getInputStream(), 204 socket.getOutputStream()); 205 } catch (IOException e) { 206 throw new RuntimeException(e); 207 } 208 Context context = this; 209 mCompanionDeviceManager.startSystemDataTransfer(mAssociationInfo.getId(), 210 getMainExecutor(), new OutcomeReceiver<>() { 211 @Override 212 public void onResult(Void result) { 213 Log.v(TAG, "Success!"); 214 Toast.makeText(context, "Success", Toast.LENGTH_LONG).show(); 215 PermissionsTransferCompanionService.sInstance.detachSystemDataTransport( 216 mAssociationInfo.getId()); 217 } 218 219 @Override 220 public void onError(CompanionException error) { 221 Log.v(TAG, "Failure!", error); 222 Toast.makeText(context, "Failed", Toast.LENGTH_LONG).show(); 223 PermissionsTransferCompanionService.sInstance.detachSystemDataTransport( 224 mAssociationInfo.getId()); 225 } 226 }); 227 } 228 229 @Override onActivityResult(int requestCode, int resultCode, Intent data)230 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 231 super.onActivityResult(requestCode, resultCode, data); 232 Log.v(TAG, "onActivityResult() request=" + requestCode + " result=" + resultCode); 233 234 switch (requestCode) { 235 case REQUEST_CODE_SYNC: 236 if (resultCode == Activity.RESULT_OK) { 237 updateAssociationInfo(mAssociationInfo); 238 startPermissionsSync(); 239 } 240 break; 241 } 242 } 243 244 @Override onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)245 public void onRequestPermissionsResult(int requestCode, 246 @NonNull String[] permissions, 247 @NonNull int[] grantResults) { 248 super.onRequestPermissionsResult(requestCode, permissions, grantResults); 249 Log.v(TAG, "onRequestPermissionsResult() request=" + requestCode 250 + " permissions=" + List.of(permissions) 251 + " result=" + List.of(grantResults)); 252 253 switch (requestCode) { 254 case REQUEST_CODE_BLUETOOTH_CONNECT_PERMISSION: 255 if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { 256 mServerThread.start(); 257 } else { 258 throw new RuntimeException("Bluetooth permission is required for this app."); 259 } 260 break; 261 } 262 } 263 264 private ServiceConnection mServiceConnection = new ServiceConnection() { 265 @Override 266 public void onServiceConnected(ComponentName name, IBinder service) { 267 // ignored 268 } 269 270 @Override 271 public void onServiceDisconnected(ComponentName name) { 272 // ignored 273 } 274 }; 275 276 private CompanionDeviceManager.Callback mCallback = new CompanionDeviceManager.Callback() { 277 @Override 278 public void onAssociationPending(@NonNull IntentSender intentSender) { 279 Log.v(TAG, "onAssociationPending " + intentSender); 280 281 try { 282 startIntentSenderForResult(intentSender, REQUEST_CODE_ASSOCIATE, null, 0, 0, 0); 283 } catch (IntentSender.SendIntentException e) { 284 throw new RuntimeException(e); 285 } 286 } 287 288 @Override 289 public void onAssociationCreated(@NonNull AssociationInfo associationInfo) { 290 Log.v(TAG, "onAssociationCreated " + associationInfo); 291 updateAssociationInfo(associationInfo); 292 } 293 294 @Override 295 public void onFailure(@Nullable CharSequence error) { 296 throw new RuntimeException(error.toString()); 297 } 298 }; 299 300 private class BluetoothServerThread extends Thread { 301 private BluetoothServerSocket mServerSocket; 302 303 @Override run()304 public void run() { 305 try { 306 Log.v(TAG, "Listening for remote connections..."); 307 mServerSocket = mAdapter.listenUsingRfcommWithServiceRecord(SERVICE_NAME, 308 SERVICE_UUID); 309 while (true) { 310 final BluetoothSocket socket = mServerSocket.accept(); 311 Log.v(TAG, "Attaching server socket " + socket); 312 PermissionsTransferCompanionService.sInstance.attachSystemDataTransport( 313 mAssociationInfo.getId(), 314 socket.getInputStream(), 315 socket.getOutputStream()); 316 } 317 } catch (IOException e) { 318 throw new RuntimeException(e); 319 } 320 } 321 shutdown()322 public void shutdown() { 323 if (mServerSocket != null) { 324 try { 325 mServerSocket.close(); 326 PermissionsTransferCompanionService.sInstance.detachSystemDataTransport( 327 mAssociationInfo.getId()); 328 } catch (IOException e) { 329 throw new RuntimeException(e); 330 } 331 } 332 } 333 } 334 } 335