Home | History | Annotate | Download | only in demorenderer
      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 
     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      * */
     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      */
    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      */
    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. */
    155     public boolean isVoicemail(String number) {
    156         return !TextUtils.isEmpty(number) && number.equals(getVoiceMailNumber());
    157     }
    158 
    159     @Nullable
    160     private String getVoiceMailNumber() {
    161         if (mVoiceMail == null) {
    162             mVoiceMail = mTelephonyManager.getVoiceMailNumber();
    163         }
    164 
    165         return mVoiceMail;
    166     }
    167 
    168     interface ContactLoadedListener {
    169         void onContactLoaded(String number, @Nullable Contact contact);
    170     }
    171 
    172     interface ContactPhotoLoadedListener {
    173         void onPhotoLoaded(int contactId, @Nullable Bitmap picture);
    174     }
    175 
    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 
    186     private void fetchPhotoAsync(int contactId, ContactPhotoLoadedListener listener) {
    187         LoadPhotoAsyncTask.createAndExecute(this, contactId, listener);
    188     }
    189 
    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 
    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 
    216         int getId() {
    217             return mId;
    218         }
    219 
    220         public String getName() {
    221             return mName;
    222         }
    223 
    224         public CharSequence getType() {
    225             return mType;
    226         }
    227 
    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 
    237         static void createAndExecute(PhoneBook phoneBook, int contactId,
    238                 ContactPhotoLoadedListener listener) {
    239             new LoadPhotoAsyncTask(phoneBook, contactId, listener)
    240                     .execute();
    241         }
    242 
    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
    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
    276         protected Bitmap doInBackground(Void... params) {
    277             return fetchBitmap(mContactId);
    278         }
    279 
    280         @Override
    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 
    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
    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