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