blob: 8a108235d26390ec63ac347228c6be3614386dfe [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.dialer.ui.common;
import android.Manifest;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
import com.android.car.dialer.livedata.SharedPreferencesLiveData;
import com.android.car.dialer.ui.common.entity.ContactSortingInfo;
import com.android.car.telephony.common.AsyncEntityLoader;
import com.android.car.telephony.common.Contact;
import com.android.car.telephony.common.InMemoryPhoneBook;
import com.android.car.telephony.common.QueryParam;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.inject.Named;
import dagger.assisted.Assisted;
import dagger.assisted.AssistedFactory;
import dagger.assisted.AssistedInject;
import dagger.hilt.android.qualifiers.ApplicationContext;
/**
* Represents a list of {@link Contact} based on the search query
*/
public class ContactResultsLiveData extends
MediatorLiveData<List<ContactResultsLiveData.ContactResultListItem>> implements
AsyncEntityLoader.EntityConverter<List<ContactResultsLiveData.ContactResultListItem>> {
private static final String[] CONTACT_DETAILS_PROJECTION = {
ContactsContract.CommonDataKinds.Phone._ID,
ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY,
ContactsContract.CommonDataKinds.Phone.NUMBER,
};
private final Context mContext;
private final SearchQueryParamProvider mSearchQueryParamProvider;
private final AsyncEntityLoader<List<ContactResultListItem>> mAsyncEntityLoader;
private final LiveData<BluetoothDevice> mCurrentHfpDeviceLiveData;
private final LiveData<String> mSearchQueryLiveData;
private final SharedPreferencesLiveData mSortOrderPreferenceLiveData;
private String mSearchQuery;
private boolean mShowOnlyOneEntry;
/**
* @param searchQueryLiveData represents a list of strings that are used to query the
* data
* @param contactListLiveData represents contact list for current connected hfp device
* @param sortOrderPreferenceLiveData has the information on how to order the acquired contacts.
* @param showOnlyOneEntry determines whether to show only entry per contact.
*/
@AssistedInject
public ContactResultsLiveData(
@ApplicationContext Context context,
LiveData<List<Contact>> contactListLiveData,
@Named("Hfp") LiveData<BluetoothDevice> currentHfpDeviceLiveData,
@Assisted LiveData<String> searchQueryLiveData,
@Assisted SharedPreferencesLiveData sortOrderPreferenceLiveData,
@Assisted boolean showOnlyOneEntry) {
mContext = context;
mShowOnlyOneEntry = showOnlyOneEntry;
mCurrentHfpDeviceLiveData = currentHfpDeviceLiveData;
mSearchQueryParamProvider = new SearchQueryParamProvider(searchQueryLiveData);
mAsyncEntityLoader = new AsyncEntityLoader<>(context, mSearchQueryParamProvider, this,
(loader, contactResultListItems) -> setValue(contactResultListItems));
addSource(contactListLiveData, this::onContactsChange);
mSearchQueryLiveData = searchQueryLiveData;
addSource(mSearchQueryLiveData, this::onSearchQueryChanged);
mSortOrderPreferenceLiveData = sortOrderPreferenceLiveData;
addSource(mSortOrderPreferenceLiveData, this::onSortOrderChanged);
}
@Override
protected void onActive() {
super.onActive();
mAsyncEntityLoader.startLoading();
}
@Override
protected void onInactive() {
super.onInactive();
mAsyncEntityLoader.reset();
}
private void onContactsChange(List<Contact> contactList) {
if (contactList == null || contactList.isEmpty()) {
mAsyncEntityLoader.cancelLoad();
setValue(Collections.emptyList());
} else {
onSearchQueryChanged(mSearchQueryLiveData.getValue());
}
}
private void onSearchQueryChanged(String searchQuery) {
mSearchQuery = searchQuery;
if (TextUtils.isEmpty(searchQuery)) {
mAsyncEntityLoader.cancelLoad();
setValue(Collections.emptyList());
} else {
mAsyncEntityLoader.forceLoad();
}
}
private void onSortOrderChanged(SharedPreferences unusedSharedPreferences) {
List<ContactResultListItem> contactResults = getValue();
sort(contactResults);
setValue(contactResults);
}
@WorkerThread
@Override
public List<ContactResultListItem> convertToEntity(@Nullable Cursor cursor) {
if (cursor == null) {
return Collections.emptyList();
}
List<ContactResultListItem> contactResults = new ArrayList<>();
BluetoothDevice currentHfpDevice = mCurrentHfpDeviceLiveData.getValue();
String accountName = currentHfpDevice == null ? null : currentHfpDevice.getAddress();
while (cursor.moveToNext()) {
int lookupKeyColIdx = cursor.getColumnIndex(
ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY);
int numberIdx = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
String lookupKey = cursor.getString(lookupKeyColIdx);
String number = cursor.getString(numberIdx);
Contact contact = InMemoryPhoneBook.get().lookupContactByKey(lookupKey, accountName);
if (contact != null) {
contactResults.add(new ContactResultListItem(contact, number, mSearchQuery));
}
}
if (mShowOnlyOneEntry) {
Set<Contact> set = new HashSet<>();
contactResults = contactResults.stream()
.filter(o -> set.add(o.getContact()))
.collect(Collectors.toList());
}
sort(contactResults);
return contactResults;
}
private void sort(List<ContactResultListItem> contactResults) {
if (contactResults != null) {
Collections.sort(contactResults, (o1, o2) -> ContactSortingInfo.getSortingInfo(
mContext, mSortOrderPreferenceLiveData).first.compare(o1.mContact,
o2.mContact));
}
}
private static class SearchQueryParamProvider implements QueryParam.Provider {
private final LiveData<String> mSearchQueryLiveData;
private SearchQueryParamProvider(LiveData<String> searchQueryLiveData) {
mSearchQueryLiveData = searchQueryLiveData;
}
@Nullable
@Override
public QueryParam getQueryParam() {
Uri lookupUri = Uri.withAppendedPath(
ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI,
Uri.encode(mSearchQueryLiveData.getValue()));
return new QueryParam(lookupUri, CONTACT_DETAILS_PROJECTION, null,
/* selectionArgs= */null, /* orderBy= */null,
Manifest.permission.READ_CONTACTS);
}
}
/**
* Represent a contact search result.
*/
public static class ContactResultListItem {
private final Contact mContact;
private final String mNumber;
private final String mSearchQuery;
public ContactResultListItem(Contact contact, String number, String searchQuery) {
mContact = contact;
mNumber = number;
mSearchQuery = searchQuery;
}
/**
* Returns the contact.
*/
public Contact getContact() {
return mContact;
}
/**
* Returns the number. It is a string read from column
* {@link ContactsContract.CommonDataKinds.Phone#NUMBER}.
*/
public String getNumber() {
return mNumber;
}
/**
* Returns the search query that initiates the search.
*/
public String getSearchQuery() {
return mSearchQuery;
}
}
/**
* Factory to create {@link ContactResultsLiveData} instances via the {@link AssistedInject}
* constructor.
*/
@AssistedFactory
public interface Factory {
/** Creates a {@link ContactResultsLiveData} instance. */
ContactResultsLiveData create(LiveData<String> searchQueryLiveData,
SharedPreferencesLiveData sortOrderPreferenceLiveData,
boolean showOnlyOneEntry);
}
}