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