blob: 42345ff841624a8c791a625f02a6d76ca9f25a32 [file] [log] [blame]
/*
* Copyright (C) 2019 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.telephony.common;
import android.content.Context;
import android.database.Cursor;
import android.provider.ContactsContract;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
/**
* A singleton statically accessible helper class which pre-loads contacts list into memory so that
* they can be accessed more easily and quickly.
*/
public class InMemoryPhoneBook implements Observer<List<Contact>> {
private static final String TAG = "CD.InMemoryPhoneBook";
private static final String KEY_FORMAT = "%s %s";
private static InMemoryPhoneBook sInMemoryPhoneBook;
private final Context mContext;
private final AsyncQueryLiveData<List<Contact>> mContactListAsyncQueryLiveData;
/**
* A map to speed up phone number searching.
*/
private final Map<I18nPhoneNumberWrapper, Contact> mPhoneNumberContactMap = new HashMap<>();
/**
* A map to look up contact by lookup key.
*/
private final Map<String, Contact> mLookupKeyContactMap = new HashMap<>();
private boolean mIsLoaded = false;
/**
* Initialize the globally accessible {@link InMemoryPhoneBook}. Returns the existing {@link
* InMemoryPhoneBook} if already initialized. {@link #tearDown()} must be called before init to
* reinitialize.
*/
public static InMemoryPhoneBook init(Context context) {
if (sInMemoryPhoneBook == null) {
sInMemoryPhoneBook = new InMemoryPhoneBook(context);
sInMemoryPhoneBook.onInit();
}
return get();
}
/**
* Returns if the InMemoryPhoneBook is initialized. get() won't return null or throw if this is
* true, but it doesn't indicate whether or not contacts are loaded yet.
* <p>
* See also: {@link #isLoaded()}
*/
public static boolean isInitialized() {
return sInMemoryPhoneBook != null;
}
/**
* Get the global {@link InMemoryPhoneBook} instance.
*/
public static InMemoryPhoneBook get() {
if (sInMemoryPhoneBook != null) {
return sInMemoryPhoneBook;
} else {
throw new IllegalStateException("Call init before get InMemoryPhoneBook");
}
}
/**
* Tears down the globally accessible {@link InMemoryPhoneBook}.
*/
public static void tearDown() {
sInMemoryPhoneBook.onTearDown();
sInMemoryPhoneBook = null;
}
private InMemoryPhoneBook(Context context) {
mContext = context;
QueryParam contactListQueryParam = new QueryParam(
ContactsContract.Data.CONTENT_URI,
null,
ContactsContract.Data.MIMETYPE + " = ? OR "
+ ContactsContract.Data.MIMETYPE + " = ?",
new String[]{
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE,
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE},
ContactsContract.Contacts.DISPLAY_NAME + " ASC ");
mContactListAsyncQueryLiveData = new AsyncQueryLiveData<List<Contact>>(mContext,
QueryParam.of(contactListQueryParam), Executors.newSingleThreadExecutor()) {
@Override
protected List<Contact> convertToEntity(Cursor cursor) {
return onCursorLoaded(cursor);
}
};
}
private void onInit() {
mContactListAsyncQueryLiveData.observeForever(this);
}
private void onTearDown() {
mContactListAsyncQueryLiveData.removeObserver(this);
}
public boolean isLoaded() {
return mIsLoaded;
}
/**
* Returns a {@link LiveData} which monitors the contact list changes.
*/
public LiveData<List<Contact>> getContactsLiveData() {
return mContactListAsyncQueryLiveData;
}
/**
* Looks up a {@link Contact} by the given phone number. Returns null if can't find a Contact or
* the {@link InMemoryPhoneBook} is still loading.
*/
@Nullable
public Contact lookupContactEntry(String phoneNumber) {
Log.v(TAG, String.format("lookupContactEntry: %s", phoneNumber));
if (!isLoaded()) {
Log.w(TAG, "looking up a contact while loading.");
}
if (TextUtils.isEmpty(phoneNumber)) {
Log.w(TAG, "looking up an empty phone number.");
return null;
}
I18nPhoneNumberWrapper i18nPhoneNumber = I18nPhoneNumberWrapper.Factory.INSTANCE.get(
mContext, phoneNumber);
return mPhoneNumberContactMap.get(i18nPhoneNumber);
}
/**
* Looks up a {@link Contact} by the given lookup key and account name. Account name could be
* null for locally added contacts. Returns null if can't find the contact entry.
*/
@Nullable
public Contact lookupContactByKey(String lookupKey, @Nullable String accountName) {
if (!isLoaded()) {
Log.w(TAG, "looking up a contact while loading.");
}
if (TextUtils.isEmpty(lookupKey)) {
Log.w(TAG, "looking up an empty lookup key.");
return null;
}
return mLookupKeyContactMap.get(getContactKey(lookupKey, accountName));
}
private List<Contact> onCursorLoaded(Cursor cursor) {
Map<String, Contact> contactMap = new LinkedHashMap<>();
List<Contact> contactList = new ArrayList<>();
while (cursor.moveToNext()) {
int accountNameColumn = cursor.getColumnIndex(
ContactsContract.RawContacts.ACCOUNT_NAME);
int lookupKeyColumn = cursor.getColumnIndex(ContactsContract.Data.LOOKUP_KEY);
String accountName = cursor.getString(accountNameColumn);
String lookupKey = cursor.getString(lookupKeyColumn);
String key = getContactKey(lookupKey, accountName);
contactMap.put(key, Contact.fromCursor(mContext, cursor, contactMap.get(key)));
}
contactList.addAll(contactMap.values());
mLookupKeyContactMap.clear();
mLookupKeyContactMap.putAll(contactMap);
for (Contact contact : contactList) {
for (PhoneNumber phoneNumber : contact.getNumbers()) {
mPhoneNumberContactMap.put(phoneNumber.getI18nPhoneNumberWrapper(), contact);
}
}
return contactList;
}
@Override
public void onChanged(List<Contact> contacts) {
Log.d(TAG, "Contacts loaded:" + (contacts == null ? 0 : contacts.size()));
mIsLoaded = true;
}
/**
* Formats a key to identify a contact based the lookup key and the account name. Account Name
* could be null and it will be handled by String.format().
*/
private String getContactKey(String lookupKey, @Nullable String accountName) {
String key = String.format(KEY_FORMAT, lookupKey, accountName);
Log.d(TAG, "Contact key is: " + key);
return key;
}
}