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.telephony.common; 18 19 import android.content.Context; 20 import android.database.Cursor; 21 import android.provider.ContactsContract; 22 import android.text.TextUtils; 23 import android.util.Log; 24 25 import androidx.annotation.NonNull; 26 import androidx.annotation.Nullable; 27 import androidx.lifecycle.LiveData; 28 import androidx.lifecycle.Observer; 29 30 import java.util.ArrayList; 31 import java.util.Collections; 32 import java.util.HashMap; 33 import java.util.LinkedHashMap; 34 import java.util.List; 35 import java.util.Map; 36 import java.util.concurrent.Executors; 37 38 /** 39 * A singleton statically accessible helper class which pre-loads contacts list into memory so that 40 * they can be accessed more easily and quickly. 41 */ 42 public class InMemoryPhoneBook implements Observer<List<Contact>> { 43 private static final String TAG = "CD.InMemoryPhoneBook"; 44 private static InMemoryPhoneBook sInMemoryPhoneBook; 45 46 private final Context mContext; 47 private final AsyncQueryLiveData<List<Contact>> mContactListAsyncQueryLiveData; 48 /** 49 * A map to speed up phone number searching. 50 */ 51 private final Map<I18nPhoneNumberWrapper, Contact> mPhoneNumberContactMap = new HashMap<>(); 52 /** 53 * A map to look up contact by account name and lookup key. Each entry presents a map of lookup 54 * key to contacts for one account. 55 */ 56 private final Map<String, Map<String, Contact>> mLookupKeyContactMap = new HashMap<>(); 57 private boolean mIsLoaded = false; 58 59 /** 60 * Initialize the globally accessible {@link InMemoryPhoneBook}. Returns the existing {@link 61 * InMemoryPhoneBook} if already initialized. {@link #tearDown()} must be called before init to 62 * reinitialize. 63 */ init(Context context)64 public static InMemoryPhoneBook init(Context context) { 65 if (sInMemoryPhoneBook == null) { 66 sInMemoryPhoneBook = new InMemoryPhoneBook(context); 67 sInMemoryPhoneBook.onInit(); 68 } 69 return get(); 70 } 71 72 /** 73 * Returns if the InMemoryPhoneBook is initialized. get() won't return null or throw if this is 74 * true, but it doesn't indicate whether or not contacts are loaded yet. 75 * <p> 76 * See also: {@link #isLoaded()} 77 */ isInitialized()78 public static boolean isInitialized() { 79 return sInMemoryPhoneBook != null; 80 } 81 82 /** 83 * Get the global {@link InMemoryPhoneBook} instance. 84 */ get()85 public static InMemoryPhoneBook get() { 86 if (sInMemoryPhoneBook != null) { 87 return sInMemoryPhoneBook; 88 } else { 89 throw new IllegalStateException("Call init before get InMemoryPhoneBook"); 90 } 91 } 92 93 /** 94 * Tears down the globally accessible {@link InMemoryPhoneBook}. 95 */ tearDown()96 public static void tearDown() { 97 sInMemoryPhoneBook.onTearDown(); 98 sInMemoryPhoneBook = null; 99 } 100 InMemoryPhoneBook(Context context)101 private InMemoryPhoneBook(Context context) { 102 mContext = context; 103 104 QueryParam contactListQueryParam = new QueryParam( 105 ContactsContract.Data.CONTENT_URI, 106 null, 107 ContactsContract.Data.MIMETYPE + " = ? OR " 108 + ContactsContract.Data.MIMETYPE + " = ? OR " 109 + ContactsContract.Data.MIMETYPE + " = ?", 110 new String[]{ 111 ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, 112 ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE, 113 ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE}, 114 ContactsContract.Contacts.DISPLAY_NAME + " ASC "); 115 mContactListAsyncQueryLiveData = new AsyncQueryLiveData<List<Contact>>(mContext, 116 QueryParam.of(contactListQueryParam), Executors.newSingleThreadExecutor()) { 117 @Override 118 protected List<Contact> convertToEntity(Cursor cursor) { 119 return onCursorLoaded(cursor); 120 } 121 }; 122 } 123 onInit()124 private void onInit() { 125 mContactListAsyncQueryLiveData.observeForever(this); 126 } 127 onTearDown()128 private void onTearDown() { 129 mContactListAsyncQueryLiveData.removeObserver(this); 130 } 131 isLoaded()132 public boolean isLoaded() { 133 return mIsLoaded; 134 } 135 136 /** 137 * Returns a {@link LiveData} which monitors the contact list changes. 138 */ getContactsLiveData()139 public LiveData<List<Contact>> getContactsLiveData() { 140 return mContactListAsyncQueryLiveData; 141 } 142 143 /** 144 * Looks up a {@link Contact} by the given phone number. Returns null if can't find a Contact or 145 * the {@link InMemoryPhoneBook} is still loading. 146 */ 147 @Nullable lookupContactEntry(String phoneNumber)148 public Contact lookupContactEntry(String phoneNumber) { 149 Log.v(TAG, String.format("lookupContactEntry: %s", phoneNumber)); 150 if (!isLoaded()) { 151 Log.w(TAG, "looking up a contact while loading."); 152 } 153 154 if (TextUtils.isEmpty(phoneNumber)) { 155 Log.w(TAG, "looking up an empty phone number."); 156 return null; 157 } 158 159 I18nPhoneNumberWrapper i18nPhoneNumber = I18nPhoneNumberWrapper.Factory.INSTANCE.get( 160 mContext, phoneNumber); 161 return mPhoneNumberContactMap.get(i18nPhoneNumber); 162 } 163 164 /** 165 * Looks up a {@link Contact} by the given lookup key and account name. Account name could be 166 * null for locally added contacts. Returns null if can't find the contact entry. 167 */ 168 @Nullable lookupContactByKey(String lookupKey, @Nullable String accountName)169 public Contact lookupContactByKey(String lookupKey, @Nullable String accountName) { 170 if (!isLoaded()) { 171 Log.w(TAG, "looking up a contact while loading."); 172 } 173 if (TextUtils.isEmpty(lookupKey)) { 174 Log.w(TAG, "looking up an empty lookup key."); 175 return null; 176 } 177 if (mLookupKeyContactMap.containsKey(accountName)) { 178 return mLookupKeyContactMap.get(accountName).get(lookupKey); 179 } 180 181 return null; 182 } 183 184 /** 185 * Iterates all the accounts and returns a list of contacts that match the lookup key. This API 186 * is discouraged to use whenever the account name is available where {@link 187 * #lookupContactByKey(String, String)} should be used instead. 188 */ 189 @NonNull lookupContactByKey(String lookupKey)190 public List<Contact> lookupContactByKey(String lookupKey) { 191 if (!isLoaded()) { 192 Log.w(TAG, "looking up a contact while loading."); 193 } 194 195 if (TextUtils.isEmpty(lookupKey)) { 196 Log.w(TAG, "looking up an empty lookup key."); 197 return Collections.emptyList(); 198 } 199 List<Contact> results = new ArrayList<>(); 200 // Iterate all the accounts to get all the match contacts with given lookup key. 201 for (Map<String, Contact> subMap : mLookupKeyContactMap.values()) { 202 if (subMap.containsKey(lookupKey)) { 203 results.add(subMap.get(lookupKey)); 204 } 205 } 206 207 return results; 208 } 209 onCursorLoaded(Cursor cursor)210 private List<Contact> onCursorLoaded(Cursor cursor) { 211 Map<String, Map<String, Contact>> contactMap = new LinkedHashMap<>(); 212 List<Contact> contactList = new ArrayList<>(); 213 214 while (cursor.moveToNext()) { 215 int accountNameColumn = cursor.getColumnIndex( 216 ContactsContract.RawContacts.ACCOUNT_NAME); 217 int lookupKeyColumn = cursor.getColumnIndex(ContactsContract.Data.LOOKUP_KEY); 218 String accountName = cursor.getString(accountNameColumn); 219 String lookupKey = cursor.getString(lookupKeyColumn); 220 221 if (!contactMap.containsKey(accountName)) { 222 contactMap.put(accountName, new HashMap<>()); 223 } 224 225 Map<String, Contact> subMap = contactMap.get(accountName); 226 subMap.put(lookupKey, Contact.fromCursor(mContext, cursor, subMap.get(lookupKey))); 227 } 228 229 for (Map<String, Contact> subMap : contactMap.values()) { 230 contactList.addAll(subMap.values()); 231 } 232 233 mLookupKeyContactMap.clear(); 234 mLookupKeyContactMap.putAll(contactMap); 235 236 mPhoneNumberContactMap.clear(); 237 for (Contact contact : contactList) { 238 for (PhoneNumber phoneNumber : contact.getNumbers()) { 239 mPhoneNumberContactMap.put(phoneNumber.getI18nPhoneNumberWrapper(), contact); 240 } 241 } 242 return contactList; 243 } 244 245 @Override onChanged(List<Contact> contacts)246 public void onChanged(List<Contact> contacts) { 247 Log.d(TAG, "Contacts loaded:" + (contacts == null ? 0 : contacts.size())); 248 mIsLoaded = true; 249 } 250 } 251