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