1 /*
2  * 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 com.android.car.companiondevicesupport.activity;
18 
19 import static com.android.car.companiondevicesupport.activity.AssociationActivity.ASSOCIATED_DEVICE_DATA_NAME_EXTRA;
20 import static com.android.car.companiondevicesupport.service.CompanionDeviceSupportService.ACTION_BIND_ASSOCIATION;
21 import static com.android.car.connecteddevice.util.SafeLog.logd;
22 import static com.android.car.connecteddevice.util.SafeLog.loge;
23 
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.app.ActivityManager;
27 import android.app.Application;
28 import android.bluetooth.BluetoothAdapter;
29 import android.content.BroadcastReceiver;
30 import android.content.ComponentName;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.content.IntentFilter;
34 import android.content.ServiceConnection;
35 import android.os.IBinder;
36 import android.os.RemoteException;
37 import android.os.UserHandle;
38 
39 import androidx.lifecycle.AndroidViewModel;
40 import androidx.lifecycle.LiveData;
41 import androidx.lifecycle.MutableLiveData;
42 import androidx.lifecycle.ViewModel;
43 
44 import com.android.car.companiondevicesupport.api.external.AssociatedDevice;
45 import com.android.car.companiondevicesupport.api.external.CompanionDevice;
46 import com.android.car.companiondevicesupport.api.external.IConnectionCallback;
47 import com.android.car.companiondevicesupport.api.external.IDeviceAssociationCallback;
48 import com.android.car.companiondevicesupport.api.internal.association.IAssociatedDeviceManager;
49 import com.android.car.companiondevicesupport.api.internal.association.IAssociationCallback;
50 import com.android.car.companiondevicesupport.service.CompanionDeviceSupportService;
51 
52 import java.util.ArrayList;
53 import java.util.List;
54 
55 /**
56  * Implementation {@link ViewModel} for sharing associated devices data between
57  * {@link AssociatedDeviceDetailFragment} and {@link AssociationActivity}
58  */
59 public class AssociatedDeviceViewModel extends AndroidViewModel {
60 
61     private static final String TAG = "AssociatedDeviceViewModel";
62 
63     public enum AssociationState { NONE, PENDING, STARTING, STARTED, COMPLETED, ERROR }
64 
65     private IAssociatedDeviceManager mAssociatedDeviceManager;
66     private List<AssociatedDevice> mAssociatedDevices = new ArrayList<>();
67     private List<CompanionDevice> mConnectedDevices = new ArrayList<>();
68 
69     private final MutableLiveData<AssociatedDeviceDetails> mDeviceDetails =
70             new MutableLiveData<>(null);
71     private final MutableLiveData<String> mAdvertisedCarName = new MutableLiveData<>(null);
72     private final MutableLiveData<String> mPairingCode = new MutableLiveData<>(null);
73     private final MutableLiveData<AssociatedDevice> mDeviceToRemove = new MutableLiveData<>(null);
74     private final MutableLiveData<Integer> mBluetoothState =
75             new MutableLiveData<>(BluetoothAdapter.STATE_OFF);
76     private final MutableLiveData<AssociationState> mAssociationState =
77             new MutableLiveData<>(AssociationState.NONE);
78     private final MutableLiveData<AssociatedDevice> mRemovedDevice = new MutableLiveData<>(null);
79     private final MutableLiveData<Boolean> mIsFinished = new MutableLiveData<>(false);
80 
AssociatedDeviceViewModel(@onNull Application application)81     public AssociatedDeviceViewModel(@NonNull Application application) {
82         super(application);
83         Intent intent = new Intent(getApplication(), CompanionDeviceSupportService.class);
84         intent.setAction(ACTION_BIND_ASSOCIATION);
85         getApplication().bindServiceAsUser(intent, mConnection, Context.BIND_AUTO_CREATE,
86                 UserHandle.SYSTEM);
87         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
88         if (adapter != null) {
89             mBluetoothState.postValue(adapter.getState());
90         }
91     }
92 
93     @Override
onCleared()94     protected void onCleared() {
95         super.onCleared();
96         try {
97             unregisterCallbacks();
98         } catch (RemoteException e) {
99             loge(TAG, "Error clearing registered callbacks. ", e);
100         }
101         getApplication().unbindService(mConnection);
102         getApplication().unregisterReceiver(mReceiver);
103         mAssociatedDeviceManager = null;
104     }
105 
106     /** Confirms that the pairing code matches. */
acceptVerification()107     public void acceptVerification() {
108         mPairingCode.postValue(null);
109         try {
110             mAssociatedDeviceManager.acceptVerification();
111         } catch (RemoteException e) {
112             loge(TAG, "Error while accepting verification.", e);
113         }
114     }
115 
116     /** Stops association. */
stopAssociation()117     public void stopAssociation() {
118         AssociationState state = mAssociationState.getValue();
119         if (state != AssociationState.STARTING && state != AssociationState.STARTED) {
120             return;
121         }
122         mAdvertisedCarName.postValue(null);
123         mPairingCode.postValue(null);
124         try {
125             mAssociatedDeviceManager.stopAssociation();
126         } catch (RemoteException e) {
127             loge(TAG, "Error while stopping association process.", e);
128         }
129         mAssociationState.postValue(AssociationState.NONE);
130     }
131 
132     /** Retry association. */
retryAssociation()133     public void retryAssociation() {
134         stopAssociation();
135         startAssociation();
136     }
137 
138     /** Select the current associated device as the device to remove. */
selectCurrentDeviceToRemove()139     public void selectCurrentDeviceToRemove() {
140         mDeviceToRemove.postValue(getAssociatedDevice());
141     }
142 
143     /** Remove the current associated device.*/
removeCurrentDevice()144     public void removeCurrentDevice() {
145         AssociatedDevice device = getAssociatedDevice();
146         if (device == null) {
147             return;
148         }
149         try {
150             mAssociatedDeviceManager.removeAssociatedDevice(device.getDeviceId());
151         } catch (RemoteException e) {
152             loge(TAG, "Failed to remove associated device: " + device, e);
153         }
154     }
155 
156     /** Toggle connection on the current associated device. */
toggleConnectionStatusForCurrentDevice()157     public void toggleConnectionStatusForCurrentDevice() {
158         AssociatedDevice device = getAssociatedDevice();
159         if (device == null) {
160             return;
161         }
162         try {
163             if (device.isConnectionEnabled()) {
164                 mAssociatedDeviceManager.disableAssociatedDeviceConnection(device.getDeviceId());
165             } else {
166                 mAssociatedDeviceManager.enableAssociatedDeviceConnection(device.getDeviceId());
167             }
168         } catch (RemoteException e) {
169             loge(TAG, "Failed to toggle connection status for device: " + device + ".", e);
170         }
171     }
172 
173     /** Get the associated device details. */
getDeviceDetails()174     public MutableLiveData<AssociatedDeviceDetails> getDeviceDetails() {
175         return mDeviceDetails;
176     }
177 
178     /** Start feature activity for the current associated device. */
startFeatureActivityForCurrentDevice(@onNull String action)179     public void startFeatureActivityForCurrentDevice(@NonNull String action) {
180         AssociatedDevice device = getAssociatedDevice();
181         if (device == null || action == null) {
182             return;
183         }
184         Intent intent = new Intent(action);
185         intent.putExtra(ASSOCIATED_DEVICE_DATA_NAME_EXTRA, device);
186         getApplication().startActivityAsUser(intent,
187                 UserHandle.of(ActivityManager.getCurrentUser()));
188     }
189 
190     /** Reset the value of {@link #mAssociationState} to {@link AssociationState.NONE}. */
resetAssociationState()191     public void resetAssociationState() {
192         mAssociationState.postValue(AssociationState.NONE);
193     }
194 
195     /** Get the associated device to remove. The associated device could be null. */
getDeviceToRemove()196     public LiveData<AssociatedDevice> getDeviceToRemove() {
197         return mDeviceToRemove;
198     }
199 
200     /** Get the name that is being advertised by the car. */
getAdvertisedCarName()201     public MutableLiveData<String> getAdvertisedCarName() {
202         return mAdvertisedCarName;
203     }
204 
205     /** Get the generated pairing code. */
getPairingCode()206     public MutableLiveData<String> getPairingCode() {
207         return mPairingCode;
208     }
209 
210     /** Value is {@code true} if the current associated device has been removed. */
getRemovedDevice()211     public MutableLiveData<AssociatedDevice> getRemovedDevice() {
212         return mRemovedDevice;
213     }
214 
215     /** Get the current {@link AssociationState}. */
getAssociationState()216     public MutableLiveData<AssociationState> getAssociationState() {
217         return mAssociationState;
218     }
219 
220     /** Get the current Bluetooth state. */
getBluetoothState()221     public MutableLiveData<Integer> getBluetoothState() {
222         return mBluetoothState;
223     }
224 
225     /** Value is {@code true} if IHU is not in association and has no associated device. */
isFinished()226     public MutableLiveData<Boolean> isFinished() {
227         return mIsFinished;
228     }
229 
updateDeviceDetails()230     private void updateDeviceDetails() {
231         AssociatedDevice device = getAssociatedDevice();
232         if (device == null) {
233             return;
234         }
235         mDeviceDetails.postValue(new AssociatedDeviceDetails(getAssociatedDevice(), isConnected()));
236     }
237 
238     @Nullable
getAssociatedDevice()239     private AssociatedDevice getAssociatedDevice() {
240         if (mAssociatedDevices.isEmpty()) {
241             return null;
242         }
243         return mAssociatedDevices.get(0);
244     }
245 
isConnected()246     private boolean isConnected() {
247         if (mAssociatedDevices.isEmpty() || mConnectedDevices.isEmpty()) {
248             return false;
249         }
250         String associatedDeviceId = mAssociatedDevices.get(0).getDeviceId();
251         String connectedDeviceId = mConnectedDevices.get(0).getDeviceId();
252         return associatedDeviceId.equals(connectedDeviceId);
253     }
254 
setAssociatedDevices(@onNull List<AssociatedDevice> associatedDevices)255     private void setAssociatedDevices(@NonNull List<AssociatedDevice> associatedDevices) {
256         mAssociatedDevices = associatedDevices;
257         updateDeviceDetails();
258     }
259 
setConnectedDevices(@onNull List<CompanionDevice> connectedDevices)260     private void setConnectedDevices(@NonNull List<CompanionDevice> connectedDevices) {
261         mConnectedDevices = connectedDevices;
262         updateDeviceDetails();
263     }
264 
addOrUpdateAssociatedDevice(@onNull AssociatedDevice device)265     private void addOrUpdateAssociatedDevice(@NonNull AssociatedDevice device) {
266         mAssociatedDevices.removeIf(d -> d.getDeviceId().equals(device.getDeviceId()));
267         mAssociatedDevices.add(device);
268         updateDeviceDetails();
269     }
270 
removeAssociatedDevice(AssociatedDevice device)271     private void removeAssociatedDevice(AssociatedDevice device) {
272         if (mAssociatedDevices.removeIf(d -> d.getDeviceId().equals(device.getDeviceId()))) {
273             mRemovedDevice.postValue(device);
274             mDeviceDetails.postValue(null);
275         }
276     }
277 
startAssociation()278     private void startAssociation() {
279         mAssociationState.postValue(AssociationState.PENDING);
280         if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) {
281             return;
282         }
283         try {
284             mAssociatedDeviceManager.startAssociation();
285 
286         } catch (RemoteException e) {
287             loge(TAG, "Failed to start association .", e);
288             mAssociationState.postValue(AssociationState.ERROR);
289         }
290         mAssociationState.postValue(AssociationState.STARTING);
291     }
292 
registerCallbacks()293     private void registerCallbacks() throws RemoteException {
294         mAssociatedDeviceManager.setAssociationCallback(mAssociationCallback);
295         mAssociatedDeviceManager.setDeviceAssociationCallback(mDeviceAssociationCallback);
296         mAssociatedDeviceManager.setConnectionCallback(mConnectionCallback);
297     }
298 
unregisterCallbacks()299     private void unregisterCallbacks() throws RemoteException {
300         mAssociatedDeviceManager.clearDeviceAssociationCallback();
301         mAssociatedDeviceManager.clearAssociationCallback();
302         mAssociatedDeviceManager.clearConnectionCallback();
303     }
304 
305     private final ServiceConnection mConnection = new ServiceConnection() {
306         @Override
307         public void onServiceConnected(ComponentName name, IBinder service) {
308             mAssociatedDeviceManager = IAssociatedDeviceManager.Stub.asInterface(service);
309             try {
310                 registerCallbacks();
311                 setConnectedDevices(mAssociatedDeviceManager.getActiveUserConnectedDevices());
312                 setAssociatedDevices(mAssociatedDeviceManager.getActiveUserAssociatedDevices());
313             } catch (RemoteException e) {
314                 loge(TAG, "Initial set failed onServiceConnected", e);
315             }
316             AssociationState state = mAssociationState.getValue();
317             if (mAssociatedDevices.isEmpty() && state != AssociationState.STARTING &&
318                     state != AssociationState.STARTED) {
319                 startAssociation();
320             }
321             logd(TAG, "Service connected:" + name.getClassName());
322             IntentFilter filter = new IntentFilter();
323             filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
324             getApplication().registerReceiver(mReceiver, filter);
325         }
326 
327         @Override
328         public void onServiceDisconnected(ComponentName name) {
329             mAssociatedDeviceManager = null;
330             logd(TAG, "Service disconnected: " + name.getClassName());
331             mIsFinished.postValue(true);
332         }
333     };
334 
335     private final IAssociationCallback mAssociationCallback = new IAssociationCallback.Stub() {
336         @Override
337         public void onAssociationStartSuccess(String deviceName) {
338             mAssociationState.postValue(AssociationState.STARTED);
339             mAdvertisedCarName.postValue(deviceName);
340         }
341 
342         @Override
343         public void onAssociationStartFailure() {
344             mAssociationState.postValue(AssociationState.ERROR);
345             loge(TAG, "Failed to start association.");
346         }
347 
348         @Override
349         public void onAssociationError(int error) throws RemoteException {
350             mAssociationState.postValue(AssociationState.ERROR);
351             loge(TAG, "Error during association: " + error + ".");
352         }
353 
354         @Override
355         public void onVerificationCodeAvailable(String code) throws RemoteException {
356             mAdvertisedCarName.postValue(null);
357             mPairingCode.postValue(code);
358         }
359 
360         @Override
361         public void onAssociationCompleted() {
362             mAssociationState.postValue(AssociationState.COMPLETED);
363         }
364     };
365 
366     private final IDeviceAssociationCallback mDeviceAssociationCallback =
367             new IDeviceAssociationCallback.Stub() {
368                 @Override
369                 public void onAssociatedDeviceAdded(AssociatedDevice device) {
370                     addOrUpdateAssociatedDevice(device);
371                 }
372 
373                 @Override
374                 public void onAssociatedDeviceRemoved(AssociatedDevice device) {
375                     removeAssociatedDevice(device);
376                 }
377 
378                 @Override
379                 public void onAssociatedDeviceUpdated(AssociatedDevice device) {
380                     addOrUpdateAssociatedDevice(device);
381                 }
382             };
383 
384     private final IConnectionCallback mConnectionCallback = new IConnectionCallback.Stub() {
385         @Override
386         public void onDeviceConnected(CompanionDevice companionDevice) {
387             mConnectedDevices.add(companionDevice);
388             updateDeviceDetails();
389         }
390 
391         @Override
392         public void onDeviceDisconnected(CompanionDevice companionDevice) {
393             mConnectedDevices.removeIf(d -> d.getDeviceId().equals(companionDevice.getDeviceId()));
394             updateDeviceDetails();
395         }
396     };
397 
398     private BroadcastReceiver mReceiver = new BroadcastReceiver() {
399         @Override
400         public void onReceive(Context context, Intent intent)  {
401             String action = intent.getAction();
402             if (!BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
403                 return;
404             }
405             int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
406             if (state != BluetoothAdapter.STATE_ON &&
407                     state != BluetoothAdapter.STATE_OFF &&
408                     state != BluetoothAdapter.ERROR) {
409                 // No need to convey any other state.
410                 return;
411             }
412             mBluetoothState.postValue(state);
413             if (state == BluetoothAdapter.STATE_ON &&
414                     mAssociationState.getValue() == AssociationState.PENDING &&
415                     mAssociatedDeviceManager != null) {
416                 startAssociation();
417             }
418         }
419     };
420 }
421