1 /*
2  * Copyright (C) 2016 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 android.car.cluster.demorenderer;
18 
19 import static android.provider.ContactsContract.Contacts.openContactPhotoInputStream;
20 
21 import android.annotation.Nullable;
22 import android.content.ContentResolver;
23 import android.content.ContentUris;
24 import android.content.Context;
25 import android.content.CursorLoader;
26 import android.content.Loader;
27 import android.content.Loader.OnLoadCompleteListener;
28 import android.content.res.Resources;
29 import android.database.Cursor;
30 import android.graphics.Bitmap;
31 import android.graphics.BitmapFactory;
32 import android.graphics.BitmapFactory.Options;
33 import android.graphics.Rect;
34 import android.net.Uri;
35 import android.os.AsyncTask;
36 import android.provider.ContactsContract;
37 import android.provider.ContactsContract.CommonDataKinds.Phone;
38 import android.provider.ContactsContract.PhoneLookup;
39 import android.telephony.PhoneNumberUtils;
40 import android.telephony.TelephonyManager;
41 import android.text.TextUtils;
42 import android.util.Log;
43 import android.util.LruCache;
44 
45 import java.io.InputStream;
46 import java.lang.ref.WeakReference;
47 import java.util.HashMap;
48 import java.util.HashSet;
49 import java.util.Locale;
50 import java.util.Set;
51 
52 /**
53  * Class that provides contact information.
54  */
55 class PhoneBook {
56 
57     private final static String TAG = PhoneBook.class.getSimpleName();
58 
59     private final ContentResolver mContentResolver;
60     private final Context mContext;
61     private final TelephonyManager mTelephonyManager;
62     private final Object mSyncContact = new Object();
63     private final Object mSyncPhoto = new Object();
64 
65     private volatile String mVoiceMail;
66 
67     private static final String[] CONTACT_ID_PROJECTION = new String[] {
68             PhoneLookup.DISPLAY_NAME,
69             PhoneLookup.TYPE,
70             PhoneLookup.LABEL,
71             PhoneLookup._ID
72     };
73 
74     private HashMap<String, Contact> mContactByNumber;
75     private LruCache<Integer, Bitmap> mContactPhotoById;
76     private Set<Integer> mContactsWithoutImage;
77 
PhoneBook(Context context, TelephonyManager telephonyManager)78     PhoneBook(Context context, TelephonyManager telephonyManager) {
79         mContentResolver = context.getContentResolver();
80         mContext = context;
81         mTelephonyManager = telephonyManager;
82     }
83 
84     /**
85      * Formats provided number according to current locale.
86      * */
getFormattedNumber(String number)87     public static String getFormattedNumber(String number) {
88         if (TextUtils.isEmpty(number)) {
89             return "";
90         }
91 
92         String countryIso = Locale.getDefault().getCountry();
93         if (countryIso == null || countryIso.length() != 2) {
94             countryIso = "US";
95         }
96         String e164 = PhoneNumberUtils.formatNumberToE164(number, countryIso);
97         String formattedNumber = PhoneNumberUtils.formatNumber(number, e164, countryIso);
98         formattedNumber = TextUtils.isEmpty(formattedNumber) ? number : formattedNumber;
99         return formattedNumber;
100     }
101 
102     /**
103      * Loads contact details for a given phone number asynchronously. It may call listener's
104      * callback function immediately if there were image in the cache.
105      */
getContactDetailsAsync(String number, ContactLoadedListener listener)106     public void getContactDetailsAsync(String number, ContactLoadedListener listener) {
107         if (number == null || number.isEmpty()) {
108             listener.onContactLoaded(number, null);
109             return;
110         }
111 
112         synchronized (mSyncContact) {
113             if (mContactByNumber == null) {
114                 mContactByNumber = new HashMap<>();
115             } else if (mContactByNumber.containsKey(number)) {
116                 listener.onContactLoaded(number, mContactByNumber.get(number));
117                 return;
118             }
119         }
120 
121         fetchContactAsync(number, listener);
122     }
123 
124     /**
125      * Loads photo for a given contactId asynchronously. It may call listener's callback function
126      * immediately if there were image in the cache.
127      */
getContactPictureAsync(int contactId, ContactPhotoLoadedListener listener)128     public void getContactPictureAsync(int contactId, ContactPhotoLoadedListener listener) {
129         synchronized (mSyncPhoto) {
130             if (mContactsWithoutImage != null && mContactsWithoutImage.contains(contactId)) {
131                 listener.onPhotoLoaded(contactId, null);
132                 return;
133             }
134 
135             if (mContactPhotoById == null) {
136                 mContactPhotoById = new LruCache<Integer, Bitmap>(4 << 20 /* 4mb */) {
137                     @Override
138                     protected int sizeOf(Integer key, Bitmap value) {
139                         return value.getByteCount();
140                     }
141                 };
142             } else {
143                 Bitmap photo = mContactPhotoById.get(contactId);
144                 if (photo != null) {
145                     listener.onPhotoLoaded(contactId, photo);
146                     return;
147                 }
148             }
149         }
150 
151         fetchPhotoAsync(contactId, listener);
152     }
153 
154     /** Returns true if given phone number is a voice mail number. */
isVoicemail(String number)155     public boolean isVoicemail(String number) {
156         return !TextUtils.isEmpty(number) && number.equals(getVoiceMailNumber());
157     }
158 
159     @Nullable
getVoiceMailNumber()160     private String getVoiceMailNumber() {
161         if (mVoiceMail == null) {
162             mVoiceMail = mTelephonyManager.getVoiceMailNumber();
163         }
164 
165         return mVoiceMail;
166     }
167 
168     interface ContactLoadedListener {
onContactLoaded(String number, @Nullable Contact contact)169         void onContactLoaded(String number, @Nullable Contact contact);
170     }
171 
172     interface ContactPhotoLoadedListener {
onPhotoLoaded(int contactId, @Nullable Bitmap picture)173         void onPhotoLoaded(int contactId, @Nullable Bitmap picture);
174     }
175 
fetchContactAsync(String number, ContactLoadedListener listener)176     private void fetchContactAsync(String number, ContactLoadedListener listener) {
177         CursorLoader cursorLoader = new CursorLoader(mContext);
178         cursorLoader.setUri(Uri.withAppendedPath(
179                 PhoneLookup.CONTENT_FILTER_URI,
180                 Uri.encode(number)));
181         cursorLoader.setProjection(CONTACT_ID_PROJECTION);
182         cursorLoader.registerListener(0, new LoadCompleteListener(this, number, listener));
183         cursorLoader.startLoading();
184     }
185 
fetchPhotoAsync(int contactId, ContactPhotoLoadedListener listener)186     private void fetchPhotoAsync(int contactId, ContactPhotoLoadedListener listener) {
187         LoadPhotoAsyncTask.createAndExecute(this, contactId, listener);
188     }
189 
cacheContactPhoto(int contactId, Bitmap bitmap)190     private void cacheContactPhoto(int contactId, Bitmap bitmap) {
191         synchronized (mSyncPhoto) {
192             if (bitmap != null) {
193                 mContactPhotoById.put(contactId, bitmap);
194             } else {
195                 if (mContactsWithoutImage == null) {
196                     mContactsWithoutImage = new HashSet<>();
197                 }
198                 mContactsWithoutImage.add(contactId);
199             }
200         }
201     }
202 
203     static class Contact {
204         private final int mId;
205         private final String mName;
206         private final CharSequence mType;
207         private final String mNumber;
208 
Contact(Resources resources, String number, int id, String name, String label, int type)209         Contact(Resources resources, String number, int id, String name, String label, int type) {
210             mNumber = number;
211             mId = id;
212             mName = name;
213             mType = Phone.getTypeLabel(resources, type, label);
214         }
215 
getId()216         int getId() {
217             return mId;
218         }
219 
getName()220         public String getName() {
221             return mName;
222         }
223 
getType()224         public CharSequence getType() {
225             return mType;
226         }
227 
getNumber()228         public String getNumber() { return mNumber; }
229     }
230 
231     private static class LoadPhotoAsyncTask extends AsyncTask<Void, Void, Bitmap> {
232 
233         private final WeakReference<PhoneBook> mPhoneBookRef;
234         private final ContactPhotoLoadedListener mListener;
235         private final int mContactId;
236 
createAndExecute(PhoneBook phoneBook, int contactId, ContactPhotoLoadedListener listener)237         static void createAndExecute(PhoneBook phoneBook, int contactId,
238                 ContactPhotoLoadedListener listener) {
239             new LoadPhotoAsyncTask(phoneBook, contactId, listener)
240                     .execute();
241         }
242 
LoadPhotoAsyncTask(PhoneBook phoneBook, int contactId, ContactPhotoLoadedListener listener)243         private LoadPhotoAsyncTask(PhoneBook phoneBook, int contactId,
244                 ContactPhotoLoadedListener listener) {
245             mPhoneBookRef = new WeakReference<>(phoneBook);
246             mContactId = contactId;
247             mListener = listener;
248         }
249 
250         @Nullable
fetchBitmap(int contactId)251         private Bitmap fetchBitmap(int contactId) {
252             Log.d(TAG, "fetchBitmap, contactId: " + contactId);
253             PhoneBook phoneBook = mPhoneBookRef.get();
254             if (phoneBook == null) {
255                 return null;
256             }
257 
258             Uri uri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId);
259             InputStream photoDataStream = openContactPhotoInputStream(
260                     phoneBook.mContentResolver, uri, true);
261             Log.d(TAG, "fetchBitmap, uri: " + uri);
262 
263             Options options = new Options();
264             options.inPreferQualityOverSpeed = true;
265             options.inScaled = false;
266             Rect nullPadding = null;
267             Bitmap photo = BitmapFactory.decodeStream(photoDataStream, nullPadding, options);
268             if (photo != null) {
269                 photo.setDensity(Bitmap.DENSITY_NONE);
270             }
271             Log.d(TAG, "bitmap fetched: " + photo);
272             return photo;
273         }
274 
275         @Override
doInBackground(Void... params)276         protected Bitmap doInBackground(Void... params) {
277             return fetchBitmap(mContactId);
278         }
279 
280         @Override
onPostExecute(Bitmap bitmap)281         protected void onPostExecute(Bitmap bitmap) {
282             PhoneBook phoneBook = mPhoneBookRef.get();
283             if (phoneBook != null) {
284                 phoneBook.cacheContactPhoto(mContactId, bitmap);
285             }
286             mListener.onPhotoLoaded(0, bitmap);
287         }
288     }
289 
290     private static class LoadCompleteListener implements OnLoadCompleteListener<Cursor> {
291         private final String mNumber;
292         private final ContactLoadedListener mContactLoadedListener;
293         private final WeakReference<PhoneBook> mPhoneBookRef;
294 
LoadCompleteListener(PhoneBook phoneBook, String number, ContactLoadedListener contactLoadedListener)295         private LoadCompleteListener(PhoneBook phoneBook, String number,
296                 ContactLoadedListener contactLoadedListener) {
297             mPhoneBookRef = new WeakReference<>(phoneBook);
298             mNumber = number;
299             mContactLoadedListener = contactLoadedListener;
300         }
301 
302         @Override
onLoadComplete(Loader<Cursor> loader, Cursor cursor)303         public void onLoadComplete(Loader<Cursor> loader, Cursor cursor) {
304             Log.d(TAG, "onLoadComplete, cursor: " + cursor);
305             PhoneBook phoneBook = mPhoneBookRef.get();
306             Contact contact = null;
307             if (cursor != null && phoneBook != null) {
308                 try {
309                     if (cursor.moveToFirst()) {
310                         int id = cursor.getInt(cursor.getColumnIndex(PhoneLookup._ID));
311                         String name = cursor
312                                 .getString(cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME));
313                         String label = cursor.getString(cursor.getColumnIndex(PhoneLookup.LABEL));
314                         int type = cursor.getInt(cursor.getColumnIndex(PhoneLookup.TYPE));
315                         Resources resources = phoneBook.mContext.getResources();
316                         contact = new Contact(resources, mNumber, id, name, label, type);
317                     }
318                 } finally {
319                     cursor.close();
320                 }
321 
322                 if (contact != null) {
323                     synchronized (phoneBook.mSyncContact) {
324                         phoneBook.mContactByNumber.put(mNumber, contact);
325                     }
326                 }
327             }
328 
329             mContactLoadedListener.onContactLoaded(mNumber, contact);
330         }
331     }
332 }
333