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