Fix data loading in recents and contacts page

Handle the case when cursor is null and null data is set from
AsyncQueryLiveData. Before the fix, there will be no update triggered
from ViewModel which is loading state. After the fix, it will show empty
state.
Fix the issue that when turning bluetooth off or turning profile
permission off in bluetooth settings page, contacts and call logs are
not cleared.

Bug: 166323672
Bug: 167158164
Test: manual

Change-Id: I168dbf0a9da84d08791d2493f2821bd594132d27
diff --git a/src/com/android/car/dialer/bluetooth/CallHistoryManager.java b/src/com/android/car/dialer/bluetooth/CallHistoryManager.java
index af1ffbb..bb4fecc 100644
--- a/src/com/android/car/dialer/bluetooth/CallHistoryManager.java
+++ b/src/com/android/car/dialer/bluetooth/CallHistoryManager.java
@@ -19,10 +19,9 @@
 import android.content.Context;
 
 import androidx.lifecycle.LiveData;
-import androidx.lifecycle.MutableLiveData;
 import androidx.lifecycle.Observer;
-import androidx.lifecycle.Transformations;
 
+import com.android.car.arch.common.LiveDataFunctions;
 import com.android.car.dialer.livedata.CallHistoryLiveData;
 import com.android.car.dialer.log.L;
 import com.android.car.telephony.common.PhoneCallLog;
@@ -67,10 +66,9 @@
     }
 
     private CallHistoryManager(Context applicationContext) {
-        mCallHistoryLiveData = Transformations.switchMap(
-                UiBluetoothMonitor.get().getFirstHfpConnectedDevice(), (device) -> device != null
-                        ? CallHistoryLiveData.newInstance(applicationContext, device.getAddress())
-                        : new MutableLiveData<>());
+        mCallHistoryLiveData = LiveDataFunctions.switchMapNonNull(
+                UiBluetoothMonitor.get().getFirstHfpConnectedDevice(),
+                device -> CallHistoryLiveData.newInstance(applicationContext, device.getAddress()));
 
         mCallHistoryObserver = o -> L.i(TAG, "Call history is updated");
 
diff --git a/src/com/android/car/dialer/ui/common/ContactResultsLiveData.java b/src/com/android/car/dialer/ui/common/ContactResultsLiveData.java
index cf2855f..4c991ce 100644
--- a/src/com/android/car/dialer/ui/common/ContactResultsLiveData.java
+++ b/src/com/android/car/dialer/ui/common/ContactResultsLiveData.java
@@ -26,9 +26,8 @@
 import androidx.annotation.Nullable;
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MediatorLiveData;
-import androidx.lifecycle.MutableLiveData;
-import androidx.lifecycle.Transformations;
 
+import com.android.car.arch.common.LiveDataFunctions;
 import com.android.car.dialer.bluetooth.UiBluetoothMonitor;
 import com.android.car.dialer.livedata.SharedPreferencesLiveData;
 import com.android.car.dialer.ui.common.entity.ContactSortingInfo;
@@ -77,11 +76,10 @@
         mObservableAsyncQuery = new ObservableAsyncQuery(mSearchQueryParamProvider,
                 context.getContentResolver(), this::onQueryFinished);
 
-        mContactListLiveData = Transformations.switchMap(
-                UiBluetoothMonitor.get().getFirstHfpConnectedDevice(), (device) -> device != null
-                        ? InMemoryPhoneBook.get()
-                            .getContactsLiveDataByAccount(device.getAddress())
-                        : new MutableLiveData<>());
+        mContactListLiveData = LiveDataFunctions.switchMapNonNull(
+                UiBluetoothMonitor.get().getFirstHfpConnectedDevice(),
+                device -> InMemoryPhoneBook.get()
+                        .getContactsLiveDataByAccount(device.getAddress()));
         addSource(mContactListLiveData, this::onContactsChange);
         mSearchQueryLiveData = searchQueryLiveData;
         addSource(mSearchQueryLiveData, this::onSearchQueryChanged);
diff --git a/src/com/android/car/dialer/ui/common/UiCallLogLiveData.java b/src/com/android/car/dialer/ui/common/UiCallLogLiveData.java
index 0709bb7..eff6712 100644
--- a/src/com/android/car/dialer/ui/common/UiCallLogLiveData.java
+++ b/src/com/android/car/dialer/ui/common/UiCallLogLiveData.java
@@ -73,16 +73,11 @@
 
         addSource(callHistoryLiveData, this::onCallHistoryChanged);
         addSource(contactListLiveData,
-                (contacts) -> onCallHistoryChanged(callHistoryLiveData.getValue()));
+                (contacts) -> onContactsChanged(callHistoryLiveData.getValue()));
         addSource(heartBeatLiveData, (trigger) -> updateRelativeTime());
     }
 
-    private void onCallHistoryChanged(List<PhoneCallLog> callLogs) {
-        // If there is no value set, don't set null value to trigger an update.
-        if (callLogs == null && getValue() == null) {
-            return;
-        }
-
+    private void onCallHistoryChanged(@Nullable List<PhoneCallLog> callLogs) {
         if (mRunnableFuture != null) {
             mRunnableFuture.cancel(true);
         }
@@ -92,6 +87,15 @@
         mRunnableFuture = mExecutorService.submit(runnable);
     }
 
+    private void onContactsChanged(List<PhoneCallLog> callLogs) {
+        // When contacts change, do not set value to trigger an update when there are no
+        // call logs loaded yet. An update will switch the loading state to loaded in the ViewModel.
+        if (getValue() == null || getValue().isEmpty()) {
+            return;
+        }
+        onCallHistoryChanged(callLogs);
+    }
+
     private void updateRelativeTime() {
         boolean hasChanged = false;
         List<Object> uiCallLogs = getValue();
@@ -132,7 +136,7 @@
     }
 
     @NonNull
-    private List<Object> convert(List<PhoneCallLog> phoneCallLogs) {
+    private List<Object> convert(@Nullable List<PhoneCallLog> phoneCallLogs) {
         if (phoneCallLogs == null) {
             return Collections.emptyList();
         }
diff --git a/src/com/android/car/dialer/ui/contact/ContactListViewModel.java b/src/com/android/car/dialer/ui/contact/ContactListViewModel.java
index eb550da..fcdbc1b 100644
--- a/src/com/android/car/dialer/ui/contact/ContactListViewModel.java
+++ b/src/com/android/car/dialer/ui/contact/ContactListViewModel.java
@@ -21,10 +21,9 @@
 import android.util.Pair;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MediatorLiveData;
-import androidx.lifecycle.MutableLiveData;
-import androidx.lifecycle.Transformations;
 
 import com.android.car.arch.common.FutureData;
 import com.android.car.arch.common.LiveDataFunctions;
@@ -35,6 +34,7 @@
 import com.android.car.telephony.common.Contact;
 import com.android.car.telephony.common.InMemoryPhoneBook;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
@@ -55,10 +55,10 @@
         mContext = application.getApplicationContext();
 
         SharedPreferencesLiveData preferencesLiveData = getSharedPreferencesLiveData();
-        LiveData<List<Contact>> contactListLiveData = Transformations.switchMap(
-                UiBluetoothMonitor.get().getFirstHfpConnectedDevice(), (device) -> device != null
-                        ? InMemoryPhoneBook.get().getContactsLiveDataByAccount(device.getAddress())
-                        : new MutableLiveData<>());
+        LiveData<List<Contact>> contactListLiveData = LiveDataFunctions.switchMapNonNull(
+                UiBluetoothMonitor.get().getFirstHfpConnectedDevice(),
+                device -> InMemoryPhoneBook.get().getContactsLiveDataByAccount(
+                        device.getAddress()));
         mSortedContactListLiveData = new SortedContactListLiveData(
                 mContext, contactListLiveData, preferencesLiveData);
         mContactList = LiveDataFunctions.loadingSwitchMap(mSortedContactListLiveData,
@@ -90,37 +90,41 @@
             mPreferencesLiveData = sharedPreferencesLiveData;
             mExecutorService = Executors.newSingleThreadExecutor();
 
-            addSource(mPreferencesLiveData, (trigger) -> updateSortedContactList());
-            addSource(mContactListLiveData, (trigger) -> updateSortedContactList());
+            addSource(mPreferencesLiveData, trigger -> onSortOrderChanged());
+            addSource(mContactListLiveData, this::sortContacts);
         }
 
-        private void updateSortedContactList() {
-            // Don't set null value to trigger an update when there is no value set.
-            if (mContactListLiveData.getValue() == null && getValue() == null) {
+        private void onSortOrderChanged() {
+            // When sort order changes, do not set value to trigger an update if there is no data
+            // set yet. An update will switch the loading state to loaded.
+            if (mContactListLiveData.getValue() == null) {
                 return;
             }
+            sortContacts(mContactListLiveData.getValue());
+        }
 
-            if (mContactListLiveData.getValue() == null
-                    || mContactListLiveData.getValue().isEmpty()) {
+        private void sortContacts(@Nullable List<Contact> contactList) {
+            if (mRunnableFuture != null) {
+                mRunnableFuture.cancel(true);
+                mRunnableFuture = null;
+            }
+
+            if (contactList == null || contactList.isEmpty()) {
                 setValue(null);
                 return;
             }
 
-            List<Contact> contactList = mContactListLiveData.getValue();
             Pair<Comparator<Contact>, Integer> contactSortingInfo = ContactSortingInfo
                     .getSortingInfo(mContext, mPreferencesLiveData);
             Comparator<Contact> comparator = contactSortingInfo.first;
             Integer sortMethod = contactSortingInfo.second;
 
-            // SingleThreadPoolExecutor is used here to avoid multiple threads sorting the list
-            // at the same time.
-            if (mRunnableFuture != null) {
-                mRunnableFuture.cancel(true);
-            }
-
             Runnable runnable = () -> {
-                Collections.sort(contactList, comparator);
-                postValue(new Pair<>(sortMethod, contactList));
+                // Make a copy of the contact list to avoid the same list being sorted at the same
+                // time since the ViewModel is not single instance but the contact LiveData is.
+                List<Contact> contactListCopy = new ArrayList<>(contactList);
+                Collections.sort(contactListCopy, comparator);
+                postValue(new Pair<>(sortMethod, contactListCopy));
             };
             mRunnableFuture = mExecutorService.submit(runnable);
         }
@@ -130,6 +134,7 @@
             super.onInactive();
             if (mRunnableFuture != null) {
                 mRunnableFuture.cancel(true);
+                mRunnableFuture = null;
             }
         }
     }