Home | History | Annotate | Download | only in incallui
      1 /*
      2  * Copyright (C) 2013 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.incallui;
     18 
     19 import android.content.ContentUris;
     20 import android.content.Context;
     21 import android.graphics.Bitmap;
     22 import android.graphics.drawable.BitmapDrawable;
     23 import android.graphics.drawable.Drawable;
     24 import android.net.Uri;
     25 import android.os.Looper;
     26 import android.provider.ContactsContract.Contacts;
     27 import android.provider.ContactsContract.CommonDataKinds.Phone;
     28 import android.telephony.PhoneNumberUtils;
     29 import android.text.TextUtils;
     30 
     31 import com.android.incallui.service.PhoneNumberService;
     32 import com.android.incalluibind.ServiceFactory;
     33 import com.android.services.telephony.common.Call;
     34 import com.android.services.telephony.common.CallIdentification;
     35 import com.android.services.telephony.common.MoreStrings;
     36 import com.google.android.collect.Lists;
     37 import com.google.android.collect.Maps;
     38 import com.google.android.collect.Sets;
     39 import com.google.common.base.Objects;
     40 import com.google.common.base.Preconditions;
     41 
     42 import java.util.HashMap;
     43 import java.util.List;
     44 import java.util.Set;
     45 
     46 /**
     47  * Class responsible for querying Contact Information for Call objects. Can perform asynchronous
     48  * requests to the Contact Provider for information as well as respond synchronously for any data
     49  * that it currently has cached from previous queries. This class always gets called from the UI
     50  * thread so it does not need thread protection.
     51  */
     52 public class ContactInfoCache implements ContactsAsyncHelper.OnImageLoadCompleteListener {
     53 
     54     private static final String TAG = ContactInfoCache.class.getSimpleName();
     55     private static final int TOKEN_UPDATE_PHOTO_FOR_CALL_STATE = 0;
     56 
     57     private final Context mContext;
     58     private final PhoneNumberService mPhoneNumberService;
     59     private final HashMap<Integer, ContactCacheEntry> mInfoMap = Maps.newHashMap();
     60     private final HashMap<Integer, Set<ContactInfoCacheCallback>> mCallBacks = Maps.newHashMap();
     61 
     62     private static ContactInfoCache sCache = null;
     63 
     64     public static synchronized ContactInfoCache getInstance(Context mContext) {
     65         if (sCache == null) {
     66             sCache = new ContactInfoCache(mContext);
     67         }
     68         return sCache;
     69     }
     70 
     71     private ContactInfoCache(Context context) {
     72         mContext = context;
     73         mPhoneNumberService = ServiceFactory.newPhoneNumberService(context);
     74     }
     75 
     76     public ContactCacheEntry getInfo(int callId) {
     77         return mInfoMap.get(callId);
     78     }
     79 
     80     public static ContactCacheEntry buildCacheEntryFromCall(Context context,
     81             CallIdentification identification, boolean isIncoming) {
     82         final ContactCacheEntry entry = new ContactCacheEntry();
     83 
     84         // TODO: get rid of caller info.
     85         final CallerInfo info = CallerInfoUtils.buildCallerInfo(context, identification);
     86         ContactInfoCache.populateCacheEntry(context, info, entry,
     87                 identification.getNumberPresentation(), isIncoming);
     88         return entry;
     89     }
     90 
     91     private class FindInfoCallback implements CallerInfoAsyncQuery.OnQueryCompleteListener {
     92         private final boolean mIsIncoming;
     93 
     94         public FindInfoCallback(boolean isIncoming) {
     95             mIsIncoming = isIncoming;
     96         }
     97 
     98         @Override
     99         public void onQueryComplete(int token, Object cookie, CallerInfo callerInfo) {
    100             final CallIdentification identification = (CallIdentification) cookie;
    101             findInfoQueryComplete(identification, callerInfo, mIsIncoming, true);
    102         }
    103     }
    104 
    105     /**
    106      * Requests contact data for the Call object passed in.
    107      * Returns the data through callback.  If callback is null, no response is made, however the
    108      * query is still performed and cached.
    109      *
    110      * @param identification The call identification
    111      * @param callback The function to call back when the call is found. Can be null.
    112      */
    113     public void findInfo(final CallIdentification identification, final boolean isIncoming,
    114             ContactInfoCacheCallback callback) {
    115         Preconditions.checkState(Looper.getMainLooper().getThread() == Thread.currentThread());
    116         Preconditions.checkNotNull(callback);
    117 
    118         final int callId = identification.getCallId();
    119         final ContactCacheEntry cacheEntry = mInfoMap.get(callId);
    120         Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
    121 
    122         // If we have a previously obtained intermediate result return that now
    123         if (cacheEntry != null) {
    124             Log.d(TAG, "Contact lookup. In memory cache hit; lookup "
    125                     + (callBacks == null ? "complete" : "still running"));
    126             callback.onContactInfoComplete(callId, cacheEntry);
    127             // If no other callbacks are in flight, we're done.
    128             if (callBacks == null) {
    129                 return;
    130             }
    131         }
    132 
    133         // If the entry already exists, add callback
    134         if (callBacks != null) {
    135             callBacks.add(callback);
    136             return;
    137         }
    138         Log.d(TAG, "Contact lookup. In memory cache miss; searching provider.");
    139         // New lookup
    140         callBacks = Sets.newHashSet();
    141         callBacks.add(callback);
    142         mCallBacks.put(callId, callBacks);
    143 
    144         /**
    145          * Performs a query for caller information.
    146          * Save any immediate data we get from the query. An asynchronous query may also be made
    147          * for any data that we do not already have. Some queries, such as those for voicemail and
    148          * emergency call information, will not perform an additional asynchronous query.
    149          */
    150         final CallerInfo callerInfo = CallerInfoUtils.getCallerInfoForCall(
    151                 mContext, identification, new FindInfoCallback(isIncoming));
    152 
    153         findInfoQueryComplete(identification, callerInfo, isIncoming, false);
    154     }
    155 
    156     private void findInfoQueryComplete(CallIdentification identification,
    157             CallerInfo callerInfo, boolean isIncoming, boolean didLocalLookup) {
    158         final int callId = identification.getCallId();
    159         int presentationMode = identification.getNumberPresentation();
    160         if (callerInfo.contactExists || callerInfo.isEmergencyNumber() || callerInfo.isVoiceMailNumber()) {
    161             presentationMode = Call.PRESENTATION_ALLOWED;
    162         }
    163 
    164         final ContactCacheEntry cacheEntry = buildEntry(mContext, callId,
    165                 callerInfo, presentationMode, isIncoming);
    166 
    167         // Add the contact info to the cache.
    168         mInfoMap.put(callId, cacheEntry);
    169         sendInfoNotifications(callId, cacheEntry);
    170 
    171         if (didLocalLookup) {
    172             if (!callerInfo.contactExists && cacheEntry.name == null &&
    173                     mPhoneNumberService != null) {
    174                 Log.d(TAG, "Contact lookup. Local contacts miss, checking remote");
    175                 final PhoneNumberServiceListener listener = new PhoneNumberServiceListener(callId);
    176                 mPhoneNumberService.getPhoneNumberInfo(cacheEntry.number, listener, listener,
    177                         isIncoming);
    178             } else if (cacheEntry.personUri != null) {
    179                 Log.d(TAG, "Contact lookup. Local contact found, starting image load");
    180                 // Load the image with a callback to update the image state.
    181                 // When the load is finished, onImageLoadComplete() will be called.
    182                 ContactsAsyncHelper.startObtainPhotoAsync(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE,
    183                         mContext, cacheEntry.personUri, ContactInfoCache.this, callId);
    184             } else {
    185                 if (callerInfo.contactExists) {
    186                     Log.d(TAG, "Contact lookup done. Local contact found, no image.");
    187                 } else if (cacheEntry.name != null) {
    188                     Log.d(TAG, "Contact lookup done. Special contact type.");
    189                 } else {
    190                     Log.d(TAG, "Contact lookup done. Local contact not found and"
    191                             + " no remote lookup service available.");
    192                 }
    193                 clearCallbacks(callId);
    194             }
    195         }
    196     }
    197 
    198     class PhoneNumberServiceListener implements PhoneNumberService.NumberLookupListener,
    199                                      PhoneNumberService.ImageLookupListener {
    200         private final int mCallId;
    201 
    202         PhoneNumberServiceListener(int callId) {
    203             mCallId = callId;
    204         }
    205 
    206         @Override
    207         public void onPhoneNumberInfoComplete(
    208                 final PhoneNumberService.PhoneNumberInfo info) {
    209             // If we got a miss, this is the end of the lookup pipeline,
    210             // so clear the callbacks and return.
    211             if (info == null) {
    212                 Log.d(TAG, "Contact lookup done. Remote contact not found.");
    213                 clearCallbacks(mCallId);
    214                 return;
    215             }
    216 
    217             ContactCacheEntry entry = new ContactCacheEntry();
    218             entry.name = info.getDisplayName();
    219             entry.number = info.getNumber();
    220             final int type = info.getPhoneType();
    221             final String label = info.getPhoneLabel();
    222             if (type == Phone.TYPE_CUSTOM) {
    223                 entry.label = label;
    224             } else {
    225                 final CharSequence typeStr = Phone.getTypeLabel(
    226                         mContext.getResources(), type, label);
    227                 entry.label = typeStr == null ? null : typeStr.toString();
    228             }
    229             final ContactCacheEntry oldEntry = mInfoMap.get(mCallId);
    230             if (oldEntry != null) {
    231                 // Location is only obtained from local lookup so persist
    232                 // the value for remote lookups. Once we have a name this
    233                 // field is no longer used; it is persisted here in case
    234                 // the UI is ever changed to use it.
    235                 entry.location = oldEntry.location;
    236             }
    237 
    238             // If no image and it's a business, switch to using the default business avatar.
    239             if (info.getImageUrl() == null && info.isBusiness()) {
    240                 Log.d(TAG, "Business has no image. Using default.");
    241                 entry.photo = mContext.getResources().getDrawable(R.drawable.business_unknown);
    242             }
    243 
    244             // Add the contact info to the cache.
    245             mInfoMap.put(mCallId, entry);
    246             sendInfoNotifications(mCallId, entry);
    247 
    248             // If there is no image then we should not expect another callback.
    249             if (info.getImageUrl() == null) {
    250                 // We're done, so clear callbacks
    251                 clearCallbacks(mCallId);
    252             }
    253         }
    254 
    255         @Override
    256         public void onImageFetchComplete(Bitmap bitmap) {
    257             onImageLoadComplete(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, null,
    258                     bitmap, (Integer) mCallId);
    259         }
    260     }
    261 
    262     /**
    263      * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface.
    264      * make sure that the call state is reflected after the image is loaded.
    265      */
    266     @Override
    267     public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
    268         Log.d(this, "Image load complete with context: ", mContext);
    269         // TODO: may be nice to update the image view again once the newer one
    270         // is available on contacts database.
    271 
    272         final int callId = (Integer) cookie;
    273         final ContactCacheEntry entry = mInfoMap.get(callId);
    274 
    275         if (entry == null) {
    276             Log.e(this, "Image Load received for empty search entry.");
    277             clearCallbacks(callId);
    278             return;
    279         }
    280         Log.d(this, "setting photo for entry: ", entry);
    281 
    282         // Conference call icons are being handled in CallCardPresenter.
    283         if (photo != null) {
    284             Log.v(this, "direct drawable: ", photo);
    285             entry.photo = photo;
    286         } else if (photoIcon != null) {
    287             Log.v(this, "photo icon: ", photoIcon);
    288             entry.photo = new BitmapDrawable(mContext.getResources(), photoIcon);
    289         } else {
    290             Log.v(this, "unknown photo");
    291             entry.photo = null;
    292         }
    293 
    294         sendImageNotifications(callId, entry);
    295         clearCallbacks(callId);
    296     }
    297 
    298     /**
    299      * Blows away the stored cache values.
    300      */
    301     public void clearCache() {
    302         mInfoMap.clear();
    303         mCallBacks.clear();
    304     }
    305 
    306     private ContactCacheEntry buildEntry(Context context, int callId,
    307             CallerInfo info, int presentation, boolean isIncoming) {
    308         // The actual strings we're going to display onscreen:
    309         Drawable photo = null;
    310 
    311         final ContactCacheEntry cce = new ContactCacheEntry();
    312         populateCacheEntry(context, info, cce, presentation, isIncoming);
    313 
    314         // This will only be true for emergency numbers
    315         if (info.photoResource != 0) {
    316             photo = context.getResources().getDrawable(info.photoResource);
    317         } else if (info.isCachedPhotoCurrent) {
    318             if (info.cachedPhoto != null) {
    319                 photo = info.cachedPhoto;
    320             } else {
    321                 photo = context.getResources().getDrawable(R.drawable.picture_unknown);
    322             }
    323         } else if (info.person_id == 0) {
    324             photo = context.getResources().getDrawable(R.drawable.picture_unknown);
    325         } else {
    326             Uri personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, info.person_id);
    327             Log.d(TAG, "- got personUri: '" + personUri + "', based on info.person_id: " +
    328                     info.person_id);
    329 
    330             if (personUri == null) {
    331                 Log.v(TAG, "personUri is null. Just use unknown picture.");
    332                 photo = context.getResources().getDrawable(R.drawable.picture_unknown);
    333             } else {
    334                 cce.personUri = personUri;
    335             }
    336         }
    337 
    338         cce.photo = photo;
    339         return cce;
    340     }
    341 
    342     /**
    343      * Populate a cache entry from a caller identification (which got converted into a caller info).
    344      */
    345     public static void populateCacheEntry(Context context, CallerInfo info, ContactCacheEntry cce,
    346             int presentation, boolean isIncoming) {
    347         Preconditions.checkNotNull(info);
    348         String displayName = null;
    349         String displayNumber = null;
    350         String displayLocation = null;
    351         String label = null;
    352         boolean isSipCall = false;
    353 
    354             // It appears that there is a small change in behaviour with the
    355             // PhoneUtils' startGetCallerInfo whereby if we query with an
    356             // empty number, we will get a valid CallerInfo object, but with
    357             // fields that are all null, and the isTemporary boolean input
    358             // parameter as true.
    359 
    360             // In the past, we would see a NULL callerinfo object, but this
    361             // ends up causing null pointer exceptions elsewhere down the
    362             // line in other cases, so we need to make this fix instead. It
    363             // appears that this was the ONLY call to PhoneUtils
    364             // .getCallerInfo() that relied on a NULL CallerInfo to indicate
    365             // an unknown contact.
    366 
    367             // Currently, infi.phoneNumber may actually be a SIP address, and
    368             // if so, it might sometimes include the "sip:" prefix. That
    369             // prefix isn't really useful to the user, though, so strip it off
    370             // if present. (For any other URI scheme, though, leave the
    371             // prefix alone.)
    372             // TODO: It would be cleaner for CallerInfo to explicitly support
    373             // SIP addresses instead of overloading the "phoneNumber" field.
    374             // Then we could remove this hack, and instead ask the CallerInfo
    375             // for a "user visible" form of the SIP address.
    376             String number = info.phoneNumber;
    377 
    378             if (!TextUtils.isEmpty(number)) {
    379                 isSipCall = PhoneNumberUtils.isUriNumber(number);
    380                 if (number.startsWith("sip:")) {
    381                     number = number.substring(4);
    382                 }
    383             }
    384 
    385             if (TextUtils.isEmpty(info.name)) {
    386                 // No valid "name" in the CallerInfo, so fall back to
    387                 // something else.
    388                 // (Typically, we promote the phone number up to the "name" slot
    389                 // onscreen, and possibly display a descriptive string in the
    390                 // "number" slot.)
    391                 if (TextUtils.isEmpty(number)) {
    392                     // No name *or* number! Display a generic "unknown" string
    393                     // (or potentially some other default based on the presentation.)
    394                     displayName = getPresentationString(context, presentation);
    395                     Log.d(TAG, "  ==> no name *or* number! displayName = " + displayName);
    396                 } else if (presentation != Call.PRESENTATION_ALLOWED) {
    397                     // This case should never happen since the network should never send a phone #
    398                     // AND a restricted presentation. However we leave it here in case of weird
    399                     // network behavior
    400                     displayName = getPresentationString(context, presentation);
    401                     Log.d(TAG, "  ==> presentation not allowed! displayName = " + displayName);
    402                 } else if (!TextUtils.isEmpty(info.cnapName)) {
    403                     // No name, but we do have a valid CNAP name, so use that.
    404                     displayName = info.cnapName;
    405                     info.name = info.cnapName;
    406                     displayNumber = number;
    407                     Log.d(TAG, "  ==> cnapName available: displayName '" + displayName +
    408                             "', displayNumber '" + displayNumber + "'");
    409                 } else {
    410                     // No name; all we have is a number. This is the typical
    411                     // case when an incoming call doesn't match any contact,
    412                     // or if you manually dial an outgoing number using the
    413                     // dialpad.
    414                     displayNumber = number;
    415 
    416                     // Display a geographical description string if available
    417                     // (but only for incoming calls.)
    418                     if (isIncoming) {
    419                         // TODO (CallerInfoAsyncQuery cleanup): Fix the CallerInfo
    420                         // query to only do the geoDescription lookup in the first
    421                         // place for incoming calls.
    422                         displayLocation = info.geoDescription; // may be null
    423                         Log.d(TAG, "Geodescrption: " + info.geoDescription);
    424                     }
    425 
    426                     Log.d(TAG, "  ==>  no name; falling back to number:"
    427                             + " displayNumber '" + displayNumber
    428                             + "', displayLocation '" + displayLocation + "'");
    429                 }
    430             } else {
    431                 // We do have a valid "name" in the CallerInfo. Display that
    432                 // in the "name" slot, and the phone number in the "number" slot.
    433                 if (presentation != Call.PRESENTATION_ALLOWED) {
    434                     // This case should never happen since the network should never send a name
    435                     // AND a restricted presentation. However we leave it here in case of weird
    436                     // network behavior
    437                     displayName = getPresentationString(context, presentation);
    438                     Log.d(TAG, "  ==> valid name, but presentation not allowed!" +
    439                             " displayName = " + displayName);
    440                 } else {
    441                     displayName = info.name;
    442                     displayNumber = number;
    443                     label = info.phoneLabel;
    444                     Log.d(TAG, "  ==>  name is present in CallerInfo: displayName '" + displayName
    445                             + "', displayNumber '" + displayNumber + "'");
    446                 }
    447             }
    448 
    449         cce.name = displayName;
    450         cce.number = displayNumber;
    451         cce.location = displayLocation;
    452         cce.label = label;
    453         cce.isSipCall = isSipCall;
    454     }
    455 
    456     /**
    457      * Sends the updated information to call the callbacks for the entry.
    458      */
    459     private void sendInfoNotifications(int callId, ContactCacheEntry entry) {
    460         final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
    461         if (callBacks != null) {
    462             for (ContactInfoCacheCallback callBack : callBacks) {
    463                 callBack.onContactInfoComplete(callId, entry);
    464             }
    465         }
    466     }
    467 
    468     private void sendImageNotifications(int callId, ContactCacheEntry entry) {
    469         final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
    470         if (callBacks != null && entry.photo != null) {
    471             for (ContactInfoCacheCallback callBack : callBacks) {
    472                 callBack.onImageLoadComplete(callId, entry);
    473             }
    474         }
    475     }
    476 
    477     private void clearCallbacks(int callId) {
    478         mCallBacks.remove(callId);
    479     }
    480 
    481     /**
    482      * Gets name strings based on some special presentation modes.
    483      */
    484     private static String getPresentationString(Context context, int presentation) {
    485         String name = context.getString(R.string.unknown);
    486         if (presentation == Call.PRESENTATION_RESTRICTED) {
    487             name = context.getString(R.string.private_num);
    488         } else if (presentation == Call.PRESENTATION_PAYPHONE) {
    489             name = context.getString(R.string.payphone);
    490         }
    491         return name;
    492     }
    493 
    494     /**
    495      * Callback interface for the contact query.
    496      */
    497     public interface ContactInfoCacheCallback {
    498         public void onContactInfoComplete(int callId, ContactCacheEntry entry);
    499         public void onImageLoadComplete(int callId, ContactCacheEntry entry);
    500     }
    501 
    502     public static class ContactCacheEntry {
    503         public String name;
    504         public String number;
    505         public String location;
    506         public String label;
    507         public Drawable photo;
    508         public boolean isSipCall;
    509         public Uri personUri; // Used for local photo load
    510 
    511         @Override
    512         public String toString() {
    513             return Objects.toStringHelper(this)
    514                     .add("name", MoreStrings.toSafeString(name))
    515                     .add("number", MoreStrings.toSafeString(number))
    516                     .add("location", MoreStrings.toSafeString(location))
    517                     .add("label", label)
    518                     .add("photo", photo)
    519                     .add("isSipCall", isSipCall)
    520                     .toString();
    521         }
    522     }
    523 }
    524