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