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.dialer.ui; 18 19 import android.annotation.IntDef; 20 import android.app.Application; 21 import android.bluetooth.BluetoothAdapter; 22 import android.bluetooth.BluetoothDevice; 23 import android.bluetooth.BluetoothProfile; 24 import android.content.Context; 25 26 import androidx.lifecycle.AndroidViewModel; 27 import androidx.lifecycle.LiveData; 28 import androidx.lifecycle.MediatorLiveData; 29 import androidx.lifecycle.MutableLiveData; 30 import androidx.lifecycle.Transformations; 31 32 import com.android.car.dialer.R; 33 import com.android.car.dialer.livedata.BluetoothHfpStateLiveData; 34 import com.android.car.dialer.livedata.BluetoothPairListLiveData; 35 import com.android.car.dialer.livedata.BluetoothStateLiveData; 36 import com.android.car.dialer.livedata.HfpDeviceListLiveData; 37 import com.android.car.dialer.log.L; 38 import com.android.car.dialer.telecom.UiBluetoothMonitor; 39 40 import java.lang.annotation.Retention; 41 import java.lang.annotation.RetentionPolicy; 42 import java.util.Set; 43 44 /** 45 * View model for {@link TelecomActivity}. 46 */ 47 public class TelecomActivityViewModel extends AndroidViewModel { 48 private static final String TAG = "CD.TelecomActivityViewModel"; 49 /** 50 * A constant which indicates that there's no Bluetooth error. 51 */ 52 public static final String NO_BT_ERROR = "NO_ERROR"; 53 54 private final Context mApplicationContext; 55 private final LiveData<String> mErrorStringLiveData; 56 private final MutableLiveData<Integer> mDialerAppStateLiveData; 57 private final LiveData<Boolean> mRefreshTabsLiveData; 58 59 private final ToolbarTitleLiveData mToolbarTitleLiveData; 60 private final MutableLiveData<Integer> mToolbarTitleMode; 61 62 private BluetoothDevice mBluetoothDevice; 63 64 /** 65 * App state indicates if bluetooth is connected or it should just show the content fragments. 66 */ 67 @IntDef({DialerAppState.DEFAULT, DialerAppState.BLUETOOTH_ERROR, 68 DialerAppState.EMERGENCY_DIALPAD}) 69 @Retention(RetentionPolicy.SOURCE) 70 public @interface DialerAppState { 71 int DEFAULT = 0; 72 int BLUETOOTH_ERROR = 1; 73 int EMERGENCY_DIALPAD = 2; 74 } 75 TelecomActivityViewModel(Application application)76 public TelecomActivityViewModel(Application application) { 77 super(application); 78 mApplicationContext = application.getApplicationContext(); 79 80 mToolbarTitleMode = new MediatorLiveData<>(); 81 mToolbarTitleLiveData = new ToolbarTitleLiveData(mApplicationContext, mToolbarTitleMode); 82 83 if (BluetoothAdapter.getDefaultAdapter() == null) { 84 MutableLiveData<String> bluetoothUnavailableLiveData = new MutableLiveData<>(); 85 bluetoothUnavailableLiveData.setValue( 86 mApplicationContext.getString(R.string.bluetooth_unavailable)); 87 mErrorStringLiveData = bluetoothUnavailableLiveData; 88 } else { 89 UiBluetoothMonitor uiBluetoothMonitor = UiBluetoothMonitor.get(); 90 mErrorStringLiveData = new ErrorStringLiveData( 91 mApplicationContext, 92 uiBluetoothMonitor.getHfpStateLiveData(), 93 uiBluetoothMonitor.getPairListLiveData(), 94 uiBluetoothMonitor.getBluetoothStateLiveData()); 95 } 96 97 mDialerAppStateLiveData = new DialerAppStateLiveData(mErrorStringLiveData); 98 99 HfpDeviceListLiveData hfpDeviceListLiveData = new HfpDeviceListLiveData(getApplication()); 100 mRefreshTabsLiveData = Transformations.map(hfpDeviceListLiveData, (hfpDeviceList) -> { 101 if (hfpDeviceList != null && !hfpDeviceList.isEmpty()) { 102 if (!hfpDeviceList.contains(mBluetoothDevice)) { 103 mBluetoothDevice = hfpDeviceList.get(0); 104 return true; 105 } 106 } else { 107 if (mBluetoothDevice != null) { 108 mBluetoothDevice = null; 109 return true; 110 } 111 } 112 return false; 113 }); 114 } 115 116 /** 117 * Returns the {@link LiveData} for the toolbar title, which provides the toolbar title 118 * depending on the {@link R.attr#toolbarTitleMode}. 119 */ getToolbarTitle()120 public LiveData<String> getToolbarTitle() { 121 return mToolbarTitleLiveData; 122 } 123 124 /** 125 * Returns the {@link MutableLiveData} of the toolbar title mode. The value should be set by the 126 * {@link TelecomActivity}. 127 */ getToolbarTitleMode()128 public MutableLiveData<Integer> getToolbarTitleMode() { 129 return mToolbarTitleMode; 130 } 131 132 /** 133 * Returns the state of Dialer App. 134 */ getDialerAppState()135 public MutableLiveData<Integer> getDialerAppState() { 136 return mDialerAppStateLiveData; 137 } 138 139 /** 140 * Returns a LiveData which provides the warning string based on Bluetooth states. Returns 141 * {@link #NO_BT_ERROR} if there's no error. 142 */ getErrorMessage()143 public LiveData<String> getErrorMessage() { 144 return mErrorStringLiveData; 145 } 146 147 /** 148 * Returns the live data which monitors whether to refresh Dialer. 149 */ getRefreshTabsLiveData()150 public LiveData<Boolean> getRefreshTabsLiveData() { 151 return mRefreshTabsLiveData; 152 } 153 154 private static class DialerAppStateLiveData extends MediatorLiveData<Integer> { 155 private final LiveData<String> mErrorStringLiveData; 156 DialerAppStateLiveData(LiveData<String> errorStringLiveData)157 private DialerAppStateLiveData(LiveData<String> errorStringLiveData) { 158 this.mErrorStringLiveData = errorStringLiveData; 159 setValue(DialerAppState.DEFAULT); 160 161 addSource(mErrorStringLiveData, errorMsg -> updateDialerAppState()); 162 } 163 updateDialerAppState()164 private void updateDialerAppState() { 165 L.d(TAG, "updateDialerAppState, error: %s", mErrorStringLiveData.getValue()); 166 167 // If bluetooth is not connected, user can make an emergency call. So show the in 168 // call fragment no matter if bluetooth is connected or not. 169 // Bluetooth error 170 if (!NO_BT_ERROR.equals(mErrorStringLiveData.getValue())) { 171 // Currently bluetooth is not connected, stay on the emergency dial pad page. 172 if (getValue() == DialerAppState.EMERGENCY_DIALPAD) { 173 return; 174 } 175 setValue(DialerAppState.BLUETOOTH_ERROR); 176 return; 177 } 178 179 // Bluetooth connected. 180 setValue(DialerAppState.DEFAULT); 181 } 182 183 @Override setValue(@ialerAppState Integer newValue)184 public void setValue(@DialerAppState Integer newValue) { 185 // Only set value and notify observers when the value changes. 186 if (getValue() != newValue) { 187 super.setValue(newValue); 188 } 189 } 190 } 191 192 private static class ErrorStringLiveData extends MediatorLiveData<String> { 193 private LiveData<Integer> mHfpStateLiveData; 194 private LiveData<Set<BluetoothDevice>> mPairedListLiveData; 195 private LiveData<Integer> mBluetoothStateLiveData; 196 197 private Context mContext; 198 ErrorStringLiveData(Context context, BluetoothHfpStateLiveData hfpStateLiveData, BluetoothPairListLiveData pairListLiveData, BluetoothStateLiveData bluetoothStateLiveData)199 ErrorStringLiveData(Context context, 200 BluetoothHfpStateLiveData hfpStateLiveData, 201 BluetoothPairListLiveData pairListLiveData, 202 BluetoothStateLiveData bluetoothStateLiveData) { 203 mContext = context; 204 mHfpStateLiveData = hfpStateLiveData; 205 mPairedListLiveData = pairListLiveData; 206 mBluetoothStateLiveData = bluetoothStateLiveData; 207 setValue(NO_BT_ERROR); 208 209 addSource(hfpStateLiveData, this::onHfpStateChanged); 210 addSource(pairListLiveData, this::onPairListChanged); 211 addSource(bluetoothStateLiveData, this::onBluetoothStateChanged); 212 } 213 onHfpStateChanged(Integer state)214 private void onHfpStateChanged(Integer state) { 215 update(); 216 } 217 onPairListChanged(Set<BluetoothDevice> pairedDevices)218 private void onPairListChanged(Set<BluetoothDevice> pairedDevices) { 219 update(); 220 } 221 onBluetoothStateChanged(Integer state)222 private void onBluetoothStateChanged(Integer state) { 223 update(); 224 } 225 update()226 private void update() { 227 boolean isBluetoothEnabled = isBluetoothEnabled(); 228 boolean hasPairedDevices = hasPairedDevices(); 229 boolean isHfpConnected = isHfpConnected(); 230 L.d(TAG, "Update error string." 231 + " isBluetoothEnabled: %s" 232 + " hasPairedDevices: %s" 233 + " isHfpConnected: %s", 234 isBluetoothEnabled, 235 hasPairedDevices, 236 isHfpConnected); 237 if (isHfpConnected) { 238 if (!NO_BT_ERROR.equals(getValue())) { 239 setValue(NO_BT_ERROR); 240 } 241 } else if (!isBluetoothEnabled) { 242 setValue(mContext.getString(R.string.bluetooth_disabled)); 243 } else if (!hasPairedDevices) { 244 setValue(mContext.getString(R.string.bluetooth_unpaired)); 245 } else { 246 setValue(mContext.getString(R.string.no_hfp)); 247 } 248 } 249 isHfpConnected()250 private boolean isHfpConnected() { 251 Integer hfpState = mHfpStateLiveData.getValue(); 252 return hfpState == null || hfpState == BluetoothProfile.STATE_CONNECTED; 253 } 254 isBluetoothEnabled()255 private boolean isBluetoothEnabled() { 256 Integer bluetoothState = mBluetoothStateLiveData.getValue(); 257 return bluetoothState == null 258 || bluetoothState != BluetoothStateLiveData.BluetoothState.DISABLED; 259 } 260 hasPairedDevices()261 private boolean hasPairedDevices() { 262 Set<BluetoothDevice> pairedDevices = mPairedListLiveData.getValue(); 263 return pairedDevices == null || !pairedDevices.isEmpty(); 264 } 265 } 266 } 267