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