Home | History | Annotate | Download | only in telecom
      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 package com.android.car.stream.telecom;
     17 
     18 import android.content.ContentResolver;
     19 import android.content.ContentUris;
     20 import android.content.Context;
     21 import android.content.res.Resources;
     22 import android.database.Cursor;
     23 import android.graphics.Bitmap;
     24 import android.graphics.BitmapFactory;
     25 import android.graphics.Rect;
     26 import android.net.Uri;
     27 import android.provider.ContactsContract;
     28 import android.support.annotation.Nullable;
     29 import android.support.annotation.WorkerThread;
     30 import android.telecom.Call;
     31 import android.telecom.GatewayInfo;
     32 import android.telephony.PhoneNumberUtils;
     33 import android.telephony.TelephonyManager;
     34 import android.text.TextUtils;
     35 import android.util.LruCache;
     36 import com.android.car.apps.common.CircleBitmapDrawable;
     37 import com.android.car.apps.common.LetterTileDrawable;
     38 import com.android.car.stream.R;
     39 
     40 import java.io.InputStream;
     41 import java.util.HashMap;
     42 import java.util.Locale;
     43 
     44 /**
     45  * Telecom related utility methods.
     46  */
     47 public class TelecomUtils {
     48     private static final int LRU_CACHE_SIZE = 4194304; /** 4 mb **/
     49 
     50     private static final String[] CONTACT_ID_PROJECTION = new String[] {
     51             ContactsContract.PhoneLookup.DISPLAY_NAME,
     52             ContactsContract.PhoneLookup.TYPE,
     53             ContactsContract.PhoneLookup.LABEL,
     54             ContactsContract.PhoneLookup._ID
     55     };
     56 
     57     private static String sVoicemailNumber;
     58 
     59     private static LruCache<String, Bitmap> sContactPhotoNumberCache;
     60     private static LruCache<Long, Bitmap> sContactPhotoIdCache;
     61     private static HashMap<String, String> sContactNameCache;
     62     private static HashMap<String, Integer> sContactIdCache;
     63     private static HashMap<String, String> sFormattedNumberCache;
     64     private static HashMap<String, String> sDisplayNameCache;
     65 
     66     /**
     67      * Create a round bitmap icon to represent the call. If a contact photo does not exist,
     68      * a letter tile will be used instead.
     69      */
     70     public static Bitmap createStreamCardSecondaryIcon(Context context, String number) {
     71         Resources res = context.getResources();
     72         Bitmap largeIcon
     73                 = TelecomUtils.getContactPhotoFromNumber(context.getContentResolver(), number);
     74         if (largeIcon == null) {
     75             LetterTileDrawable ltd = new LetterTileDrawable(res);
     76             String name = TelecomUtils.getDisplayName(context, number);
     77             ltd.setContactDetails(name, number);
     78             ltd.setIsCircular(true);
     79             int size = res.getDimensionPixelSize(R.dimen.stream_card_secondary_icon_dimen);
     80             largeIcon = ltd.toBitmap(size);
     81         }
     82 
     83         return new CircleBitmapDrawable(res, largeIcon)
     84                 .toBitmap(res.getDimensionPixelSize(R.dimen.stream_card_secondary_icon_dimen));
     85     }
     86 
     87 
     88     /**
     89      * Fetch contact photo by number from local cache.
     90      *
     91      * @param number
     92      * @return Contact photo if it's in the cache, otherwise null.
     93      */
     94     @Nullable
     95     public static Bitmap getCachedContactPhotoFromNumber(String number) {
     96         if (number == null) {
     97             return null;
     98         }
     99 
    100         if (sContactPhotoNumberCache == null) {
    101             sContactPhotoNumberCache = new LruCache<String, Bitmap>(LRU_CACHE_SIZE) {
    102                 @Override
    103                 protected int sizeOf(String key, Bitmap value) {
    104                     return value.getByteCount();
    105                 }
    106             };
    107         }
    108         return sContactPhotoNumberCache.get(number);
    109     }
    110 
    111     @WorkerThread
    112     public static Bitmap getContactPhotoFromNumber(ContentResolver contentResolver, String number) {
    113         if (number == null) {
    114             return null;
    115         }
    116 
    117         Bitmap photo = getCachedContactPhotoFromNumber(number);
    118         if (photo != null) {
    119             return photo;
    120         }
    121 
    122         int id = getContactIdFromNumber(contentResolver, number);
    123         if (id == 0) {
    124             return null;
    125         }
    126         photo = getContactPhotoFromId(contentResolver, id);
    127         if (photo != null) {
    128             sContactPhotoNumberCache.put(number, photo);
    129         }
    130         return photo;
    131     }
    132 
    133     /**
    134      * Return the contact id for the given contact id
    135      * @param id the contact id to get the photo for
    136      * @return the contact photo if it is found, null otherwise.
    137      */
    138     public static Bitmap getContactPhotoFromId(ContentResolver contentResolver, long id) {
    139         if (sContactPhotoIdCache == null) {
    140             sContactPhotoIdCache = new LruCache<Long, Bitmap>(LRU_CACHE_SIZE) {
    141                 @Override
    142                 protected int sizeOf(Long key, Bitmap value) {
    143                     return value.getByteCount();
    144                 }
    145             };
    146         } else if (sContactPhotoIdCache.get(id) != null) {
    147             return sContactPhotoIdCache.get(id);
    148         }
    149 
    150         Uri photoUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id);
    151         InputStream photoDataStream = ContactsContract.Contacts.openContactPhotoInputStream(
    152                 contentResolver, photoUri, true);
    153 
    154         BitmapFactory.Options options = new BitmapFactory.Options();
    155         options.inPreferQualityOverSpeed = true;
    156         // Scaling will be handled by later. We shouldn't scale multiple times to avoid
    157         // quality lost due to multiple potential scaling up and down.
    158         options.inScaled = false;
    159 
    160         Rect nullPadding = null;
    161         Bitmap photo = BitmapFactory.decodeStream(photoDataStream, nullPadding, options);
    162         if (photo != null) {
    163             photo.setDensity(Bitmap.DENSITY_NONE);
    164             sContactPhotoIdCache.put(id, photo);
    165         }
    166         return photo;
    167     }
    168 
    169     /**
    170      * Return the contact id for the given phone number.
    171      * @param number Caller phone number
    172      * @return the contact id if it is found, 0 otherwise.
    173      */
    174     public static int getContactIdFromNumber(ContentResolver cr, String number) {
    175         if (number == null || number.isEmpty()) {
    176             return 0;
    177         }
    178         if (sContactIdCache == null) {
    179             sContactIdCache = new HashMap<>();
    180         } else if (sContactIdCache.containsKey(number)) {
    181             return sContactIdCache.get(number);
    182         }
    183 
    184         Uri uri = Uri.withAppendedPath(
    185                 ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
    186                 Uri.encode(number));
    187         Cursor cursor = cr.query(uri, CONTACT_ID_PROJECTION, null, null, null);
    188 
    189         try {
    190             if (cursor != null && cursor.moveToFirst()) {
    191                 int id = cursor.getInt(cursor.getColumnIndex(ContactsContract.PhoneLookup._ID));
    192                 sContactIdCache.put(number, id);
    193                 return id;
    194             }
    195         }
    196         finally {
    197             if (cursor != null) {
    198                 cursor.close();
    199             }
    200         }
    201         return 0;
    202     }
    203 
    204     public static String getDisplayName(Context context, String number) {
    205         return getDisplayName(context, number, (Uri)null);
    206     }
    207 
    208     public static String getDisplayName(Context context, Call call) {
    209         // A call might get created before its children are added. In that case, the display name
    210         // would go from "Unknown" to "Conference call" therefore we don't want to cache it.
    211         if (call.getChildren() != null && call.getChildren().size() > 0) {
    212             return context.getString(R.string.conference_call);
    213         }
    214         return getDisplayName(context, getNumber(call), getGatewayInfoOriginalAddress(call));
    215     }
    216 
    217     private static Uri getGatewayInfoOriginalAddress(Call call) {
    218         if (call == null || call.getDetails() == null) {
    219             return null;
    220         }
    221         GatewayInfo gatewayInfo = call.getDetails().getGatewayInfo();
    222 
    223         if (gatewayInfo != null && gatewayInfo.getOriginalAddress() != null) {
    224             return gatewayInfo.getGatewayAddress();
    225         }
    226         return null;
    227     }
    228 
    229     /**
    230      * Return the phone number of the call. This CAN return null under certain circumstances such
    231      * as if the incoming number is hidden.
    232      */
    233     public static String getNumber(Call call) {
    234         if (call == null || call.getDetails() == null) {
    235             return null;
    236         }
    237 
    238         Uri gatewayInfoOriginalAddress = getGatewayInfoOriginalAddress(call);
    239         if (gatewayInfoOriginalAddress != null) {
    240             return gatewayInfoOriginalAddress.getSchemeSpecificPart();
    241         }
    242 
    243         if (call.getDetails().getHandle() != null) {
    244             return call.getDetails().getHandle().getSchemeSpecificPart();
    245         }
    246         return null;
    247     }
    248 
    249     private static String getContactNameFromNumber(ContentResolver cr, String number) {
    250         if (sContactNameCache == null) {
    251             sContactNameCache = new HashMap<>();
    252         } else if (sContactNameCache.containsKey(number)) {
    253             return sContactNameCache.get(number);
    254         }
    255 
    256         Uri uri = Uri.withAppendedPath(
    257                 ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
    258 
    259         Cursor cursor = null;
    260         String name = null;
    261         try {
    262             cursor = cr.query(uri,
    263                     new String[] {ContactsContract.PhoneLookup.DISPLAY_NAME}, null, null, null);
    264             if (cursor != null && cursor.moveToFirst()) {
    265                 name = cursor.getString(0);
    266                 sContactNameCache.put(number, name);
    267             }
    268         } finally {
    269             if (cursor != null) {
    270                 cursor.close();
    271             }
    272         }
    273         return name;
    274     }
    275 
    276     private static String getDisplayName(
    277             Context context, String number, Uri gatewayOriginalAddress) {
    278         if (sDisplayNameCache == null) {
    279             sDisplayNameCache = new HashMap<>();
    280         } else {
    281             if (sDisplayNameCache.containsKey(number)) {
    282                 return sDisplayNameCache.get(number);
    283             }
    284         }
    285 
    286         if (TextUtils.isEmpty(number)) {
    287             return context.getString(R.string.unknown);
    288         }
    289         ContentResolver cr = context.getContentResolver();
    290         String name;
    291         if (number.equals(getVoicemailNumber(context))) {
    292             name = context.getString(R.string.voicemail);
    293         } else {
    294             name = getContactNameFromNumber(cr, number);
    295         }
    296 
    297         if (name == null) {
    298             name = getFormattedNumber(context, number);
    299         }
    300         if (name == null && gatewayOriginalAddress != null) {
    301             name = gatewayOriginalAddress.getSchemeSpecificPart();
    302         }
    303         if (name == null) {
    304             name = context.getString(R.string.unknown);
    305         }
    306         sDisplayNameCache.put(number, name);
    307         return name;
    308     }
    309 
    310     public static String getVoicemailNumber(Context context) {
    311         if (sVoicemailNumber == null) {
    312             TelephonyManager tm =
    313                     (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
    314             sVoicemailNumber = tm.getVoiceMailNumber();
    315         }
    316         return sVoicemailNumber;
    317     }
    318 
    319     public static String getFormattedNumber(Context context, @Nullable String number) {
    320         if (TextUtils.isEmpty(number)) {
    321             return "";
    322         }
    323 
    324         if (sFormattedNumberCache == null) {
    325             sFormattedNumberCache = new HashMap<>();
    326         } else {
    327             if (sFormattedNumberCache.containsKey(number)) {
    328                 return sFormattedNumberCache.get(number);
    329             }
    330         }
    331 
    332         String countryIso = getSimRegionCode(context);
    333         String e164 = PhoneNumberUtils.formatNumberToE164(number, countryIso);
    334         String formattedNumber = PhoneNumberUtils.formatNumber(number, e164, countryIso);
    335         formattedNumber = TextUtils.isEmpty(formattedNumber) ? number : formattedNumber;
    336         sFormattedNumberCache.put(number, formattedNumber);
    337         return formattedNumber;
    338     }
    339 
    340     /**
    341      * Wrapper around TelephonyManager.getSimCountryIso() that will fallback to locale or USA ISOs
    342      * if it finds bogus data.
    343      */
    344     private static String getSimRegionCode(Context context) {
    345         TelephonyManager telephonyManager =
    346                 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
    347 
    348         // This can be null on some phones (and is null on robolectric default TelephonyManager)
    349         String countryIso = telephonyManager.getSimCountryIso();
    350         if (TextUtils.isEmpty(countryIso) || countryIso.length() != 2) {
    351             countryIso = Locale.getDefault().getCountry();
    352             if (countryIso == null || countryIso.length() != 2) {
    353                 countryIso = "US";
    354             }
    355         }
    356 
    357         return countryIso.toUpperCase(Locale.US);
    358     }
    359 }