From d2bd93fe3929403f84367f826f07cbadc0c6366b Mon Sep 17 00:00:00 2001 From: Santos Cordon Date: Mon, 12 Aug 2013 16:31:35 -0700 Subject: Add Simplifying layer between Contact Info search and CallCardProvider. I will be getting contact info on notifications for the next CL so this creates a simpler layer for interacting with the contact data async requester. CHANGES: - Moved the code which does the requesting from CallCardPresenter to ContactInfoCache. - ContactInfo Cache defines new listening interface and new simpler object for transmitting the return data: ContactInfoEntry. - Updated CallCardPresenter to use the new simpler interface. - Updated some logging entries. - Simplified Ui interface inside for CallCardPresenter. Change-Id: Ic802c4e53cdf17fcd37c70deb6da61a78b9d8993 --- .../src/com/android/incallui/CallCardFragment.java | 120 ++++--- .../com/android/incallui/CallCardPresenter.java | 358 +++++--------------- InCallUI/src/com/android/incallui/CallList.java | 1 - .../src/com/android/incallui/CallerInfoUtils.java | 46 ++- .../src/com/android/incallui/ContactInfoCache.java | 375 +++++++++++++++++++++ .../com/android/incallui/ContactsAsyncHelper.java | 33 +- InCallUI/src/com/android/incallui/Logger.java | 16 +- 7 files changed, 563 insertions(+), 386 deletions(-) create mode 100644 InCallUI/src/com/android/incallui/ContactInfoCache.java (limited to 'InCallUI') diff --git a/InCallUI/src/com/android/incallui/CallCardFragment.java b/InCallUI/src/com/android/incallui/CallCardFragment.java index b4fc852cf..6caf0089c 100644 --- a/InCallUI/src/com/android/incallui/CallCardFragment.java +++ b/InCallUI/src/com/android/incallui/CallCardFragment.java @@ -46,6 +46,7 @@ public class CallCardFragment extends BaseFragment private TextView mCallStateLabel; private ViewStub mSecondaryCallInfo; private TextView mSecondaryCallName; + private ImageView mSecondaryPhoto; // Cached DisplayMetrics density. private float mDensity; @@ -100,53 +101,67 @@ public class CallCardFragment extends BaseFragment } @Override - public void setSecondaryCallInfo(boolean show, String number) { - if (show) { - showAndInitializeSecondaryCallInfo(); - - // Until we have the name source, use the number as the main text for secondary calls. - mSecondaryCallName.setText(number); - } else { - mSecondaryCallInfo.setVisibility(View.GONE); + public void setPrimary(String number, String name, String label, Drawable photo) { + boolean nameIsNumber = false; + + // If there is no name, then use the number as the name; + if (TextUtils.isEmpty(name)) { + name = number; + number = null; + nameIsNumber = true; } - } - @Override - public void setNumber(String number) { - if (!TextUtils.isEmpty(number)) { + // Set the number + if (TextUtils.isEmpty(number)) { + mPhoneNumber.setText(""); + mPhoneNumber.setVisibility(View.GONE); + } else { mPhoneNumber.setText(number); mPhoneNumber.setVisibility(View.VISIBLE); - // We have a real phone number as "mPhoneNumber" so make it always LTR mPhoneNumber.setTextDirection(View.TEXT_DIRECTION_LTR); - } else { - mPhoneNumber.setVisibility(View.GONE); } - } - @Override - public void setName(String name, boolean isNumber) { - mName.setText(name); - mName.setVisibility(View.VISIBLE); - if (isNumber) { - mName.setTextDirection(View.TEXT_DIRECTION_LTR); + // Set direction of the name field + + // set the name field. + if (TextUtils.isEmpty(name)) { + mName.setText(""); } else { - mName.setTextDirection(View.TEXT_DIRECTION_INHERIT); - } - } + mName.setText(name); - @Override - public void setName(String name) { - setName(name, false); - } + int nameDirection = View.TEXT_DIRECTION_INHERIT; + if (nameIsNumber) { + nameDirection = View.TEXT_DIRECTION_LTR; + } + mName.setTextDirection(nameDirection); + } - @Override - public void setNumberLabel(String label) { + // Set the label (Mobile, Work, etc) if (!TextUtils.isEmpty(label)) { mNumberLabel.setText(label); mNumberLabel.setVisibility(View.VISIBLE); } else { mNumberLabel.setVisibility(View.GONE); } + + setDrawableToImageView(mPhoto, photo); + } + + @Override + public void setSecondary(boolean show, String number, String name, String label, + Drawable photo) { + + if (show) { + showAndInitializeSecondaryCallInfo(); + if (TextUtils.isEmpty(name)) { + name = number; + } + + mSecondaryCallName.setText(name); + setDrawableToImageView(mSecondaryPhoto, photo); + } else { + mSecondaryCallInfo.setVisibility(View.GONE); + } } @Override @@ -180,6 +195,22 @@ public class CallCardFragment extends BaseFragment } } + private void setDrawableToImageView(ImageView view, Drawable photo) { + if (photo == null) { + mPhoto.setVisibility(View.INVISIBLE); + return; + } + + final Drawable current = view.getDrawable(); + if (current == null) { + view.setImageDrawable(photo); + AnimationUtils.Fade.show(view); + } else { + AnimationUtils.startCrossFade(view, current, photo); + mPhoto.setVisibility(View.VISIBLE); + } + } + private void setBluetoothOn(boolean onOff) { // Also, display a special icon (alongside the "Incoming call" // label) if there's an incoming call and audio will be routed @@ -323,31 +354,8 @@ public class CallCardFragment extends BaseFragment if (mSecondaryCallName == null) { mSecondaryCallName = (TextView) getView().findViewById(R.id.secondaryCallName); } - } - - @Override - public void setImage(int resource) { - setImage(getActivity().getResources().getDrawable(resource)); - } - - @Override - public void setImage(Drawable drawable) { - setDrawableToImageView(mPhoto, drawable); - } - - @Override - public void setImage(Bitmap bitmap) { - setImage(new BitmapDrawable(getActivity().getResources(), bitmap)); - } - - private void setDrawableToImageView(ImageView view, Drawable drawable) { - final Drawable current = view.getDrawable(); - if (current == null) { - view.setImageDrawable(drawable); - AnimationUtils.Fade.show(view); - } else { - AnimationUtils.startCrossFade(view, current, drawable); - mPhoto.setVisibility(View.VISIBLE); + if (mSecondaryPhoto == null) { + mSecondaryPhoto = (ImageView) getView().findViewById(R.id.secondaryCallPhoto); } } } diff --git a/InCallUI/src/com/android/incallui/CallCardPresenter.java b/InCallUI/src/com/android/incallui/CallCardPresenter.java index d4a39eaab..105e3482a 100644 --- a/InCallUI/src/com/android/incallui/CallCardPresenter.java +++ b/InCallUI/src/com/android/incallui/CallCardPresenter.java @@ -16,15 +16,12 @@ package com.android.incallui; -import android.content.ContentUris; import android.content.Context; -import android.graphics.Bitmap; import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.provider.ContactsContract.Contacts; -import android.text.TextUtils; import com.android.incallui.AudioModeProvider.AudioModeListener; +import com.android.incallui.ContactInfoCache.ContactCacheEntry; +import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback; import com.android.incallui.InCallPresenter.InCallState; import com.android.incallui.InCallPresenter.InCallStateListener; @@ -35,27 +32,18 @@ import com.android.services.telephony.common.Call; * Presenter for the Call Card Fragment. * This class listens for changes to InCallState and passes it along to the fragment. */ -public class CallCardPresenter extends Presenter implements - InCallStateListener, CallerInfoAsyncQuery.OnQueryCompleteListener, - ContactsAsyncHelper.OnImageLoadCompleteListener, AudioModeListener { - - private static final int TOKEN_UPDATE_PHOTO_FOR_CALL_STATE = 0; +public class CallCardPresenter extends Presenter + implements InCallStateListener, AudioModeListener, ContactInfoCacheCallback { private Context mContext; private AudioModeProvider mAudioModeProvider; + private ContactInfoCache mContactInfoCache; private Call mPrimary; - - /** - * Uri being used to load contact photo for mPhoto. Will be null when nothing is being loaded, - * or a photo is already loaded. - */ - private Uri mLoadingPersonUri; - - // Track the state for the photo. - private ContactsAsyncHelper.ImageTracker mPhotoTracker; + private Call mSecondary; + private ContactCacheEntry mPrimaryContactInfo; + private ContactCacheEntry mSecondaryContactInfo; public CallCardPresenter() { - mPhotoTracker = new ContactsAsyncHelper.ImageTracker(); } @Override @@ -75,10 +63,14 @@ public class CallCardPresenter extends Presenter i mAudioModeProvider.removeListener(this); } mPrimary = null; + mPrimaryContactInfo = null; + mSecondaryContactInfo = null; } public void setContext(Context context) { mContext = context; + mContactInfoCache = new ContactInfoCache(mContext); + startContactInfoSearch(); } @Override @@ -107,32 +99,23 @@ public class CallCardPresenter extends Presenter i Logger.d(this, "Primary call: " + primary); Logger.d(this, "Secondary call: " + secondary); + mPrimary = primary; + mSecondary = secondary; - if (primary != null) { - // Set primary call data - final CallerInfo primaryCallInfo = CallerInfoUtils.getCallerInfoForCall(mContext, - primary, null, this); - updateDisplayByCallerInfo(primary, primaryCallInfo, primary.getNumberPresentation(), - true); + // Query for contact data. This will call back on onContactInfoComplete at least once + // synchronously, and potentially a second time asynchronously if it needs to make + // a full query for the data. + // It is in that callback that we set the values into the Ui. + startContactInfoSearch(); + // Set the call state + if (mPrimary != null) { final boolean bluetoothOn = mAudioModeProvider != null && mAudioModeProvider.getAudioMode() == AudioMode.BLUETOOTH; - - ui.setNumber(primary.getNumber()); - ui.setCallState(primary.getState(), primary.getDisconnectCause(), bluetoothOn); + ui.setCallState(mPrimary.getState(), mPrimary.getDisconnectCause(), bluetoothOn); } else { - ui.setNumber(""); ui.setCallState(Call.State.INVALID, Call.DisconnectCause.UNKNOWN, false); } - - // Set secondary call data - if (secondary != null) { - ui.setSecondaryCallInfo(true, secondary.getNumber()); - } else { - ui.setSecondaryCallInfo(false, null); - } - - mPrimary = primary; } @Override @@ -148,6 +131,25 @@ public class CallCardPresenter extends Presenter i public void onSupportedAudioMode(int mask) { } + /** + * Starts a query for more contact data for the save primary and secondary calls. + */ + private void startContactInfoSearch() { + if (mPrimary != null && mContactInfoCache != null) { + mContactInfoCache.findInfo(mPrimary, this); + } else { + mPrimaryContactInfo = null; + updatePrimaryDisplayInfo(); + } + + if (mSecondary != null && mContactInfoCache != null) { + mContactInfoCache.findInfo(mSecondary, this); + } else { + mSecondaryContactInfo = null; + updateSecondaryDisplayInfo(); + } + } + /** * Get the highest priority call to display. * Goes through the calls and chooses which to return based on priority of which type of call @@ -183,251 +185,56 @@ public class CallCardPresenter extends Presenter i return retval; } - public interface CallCardUi extends Ui { - // TODO(klp): Consider passing in the Call object directly in these methods. - void setVisible(boolean on); - void setNumber(String number); - void setNumberLabel(String label); - void setName(String name); - void setName(String name, boolean isNumber); - void setImage(int resource); - void setImage(Drawable drawable); - void setImage(Bitmap bitmap); - void setSecondaryCallInfo(boolean show, String number); - void setCallState(int state, Call.DisconnectCause cause, boolean bluetoothOn); - } - - @Override - public void onQueryComplete(int token, Object cookie, CallerInfo ci) { - if (cookie instanceof Call) { - final Call call = (Call) cookie; - if (ci.contactExists || ci.isEmergencyNumber() || ci.isVoiceMailNumber()) { - updateDisplayByCallerInfo(call, ci, Call.PRESENTATION_ALLOWED, true); - } else { - // If the contact doesn't exist, we can still use information from the - // returned caller info (geodescription, etc). - updateDisplayByCallerInfo(call, ci, call.getNumberPresentation(), true); - } - - // Todo (klp): updatePhotoForCallState(call); - } - } - /** - * Based on the given caller info, determine a suitable name, phone number and label - * to be passed to the CallCardUI. - * - * If the current call is a conference call, use - * updateDisplayForConference() instead. + * Callback received when Contact info data query completes. */ - private void updateDisplayByCallerInfo(Call call, CallerInfo info, int presentation, - boolean isPrimary) { - - // Inform the state machine that we are displaying a photo. - mPhotoTracker.setPhotoRequest(info); - mPhotoTracker.setPhotoState(ContactsAsyncHelper.ImageTracker.DISPLAY_IMAGE); - - // The actual strings we're going to display onscreen: - String displayName; - String displayNumber = null; - String label = null; - Uri personUri = null; - - // Gather missing info unless the call is generic, in which case we wouldn't use - // the gathered information anyway. - if (info != null) { - - // It appears that there is a small change in behaviour with the - // PhoneUtils' startGetCallerInfo whereby if we query with an - // empty number, we will get a valid CallerInfo object, but with - // fields that are all null, and the isTemporary boolean input - // parameter as true. - - // In the past, we would see a NULL callerinfo object, but this - // ends up causing null pointer exceptions elsewhere down the - // line in other cases, so we need to make this fix instead. It - // appears that this was the ONLY call to PhoneUtils - // .getCallerInfo() that relied on a NULL CallerInfo to indicate - // an unknown contact. - - // Currently, infi.phoneNumber may actually be a SIP address, and - // if so, it might sometimes include the "sip:" prefix. That - // prefix isn't really useful to the user, though, so strip it off - // if present. (For any other URI scheme, though, leave the - // prefix alone.) - // TODO: It would be cleaner for CallerInfo to explicitly support - // SIP addresses instead of overloading the "phoneNumber" field. - // Then we could remove this hack, and instead ask the CallerInfo - // for a "user visible" form of the SIP address. - String number = info.phoneNumber; - if ((number != null) && number.startsWith("sip:")) { - number = number.substring(4); - } - - if (TextUtils.isEmpty(info.name)) { - // No valid "name" in the CallerInfo, so fall back to - // something else. - // (Typically, we promote the phone number up to the "name" slot - // onscreen, and possibly display a descriptive string in the - // "number" slot.) - if (TextUtils.isEmpty(number)) { - // No name *or* number! Display a generic "unknown" string - // (or potentially some other default based on the presentation.) - displayName = getPresentationString(presentation); - Logger.d(this, " ==> no name *or* number! displayName = " + displayName); - } else if (presentation != Call.PRESENTATION_ALLOWED) { - // This case should never happen since the network should never send a phone # - // AND a restricted presentation. However we leave it here in case of weird - // network behavior - displayName = getPresentationString(presentation); - Logger.d(this, " ==> presentation not allowed! displayName = " + displayName); - } else if (!TextUtils.isEmpty(info.cnapName)) { - // No name, but we do have a valid CNAP name, so use that. - displayName = info.cnapName; - info.name = info.cnapName; - displayNumber = number; - Logger.d(this, " ==> cnapName available: displayName '" - + displayName + "', displayNumber '" + displayNumber + "'"); - } else { - // No name; all we have is a number. This is the typical - // case when an incoming call doesn't match any contact, - // or if you manually dial an outgoing number using the - // dialpad. - - // Promote the phone number up to the "name" slot: - displayName = number; - - // ...and use the "number" slot for a geographical description - // string if available (but only for incoming calls.) - if ((call != null) && (call.getState() == Call.State.INCOMING)) { - // TODO (CallerInfoAsyncQuery cleanup): Fix the CallerInfo - // query to only do the geoDescription lookup in the first - // place for incoming calls. - displayNumber = info.geoDescription; // may be null - Logger.d(this, "Geodescrption: " + info.geoDescription); - } - - Logger.d(this, " ==> no name; falling back to number: displayName '" - + displayName + "', displayNumber '" + displayNumber + "'"); - } - } else { - // We do have a valid "name" in the CallerInfo. Display that - // in the "name" slot, and the phone number in the "number" slot. - if (presentation != Call.PRESENTATION_ALLOWED) { - // This case should never happen since the network should never send a name - // AND a restricted presentation. However we leave it here in case of weird - // network behavior - displayName = getPresentationString(presentation); - Logger.d(this, " ==> valid name, but presentation not allowed!" - + " displayName = " + displayName); - } else { - displayName = info.name; - displayNumber = number; - label = info.phoneLabel; - Logger.d(this, " ==> name is present in CallerInfo: displayName '" - + displayName + "', displayNumber '" + displayNumber + "'"); - } - } - personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, info.person_id); - Logger.d(this, "- got personUri: '" + personUri - + "', based on info.person_id: " + info.person_id); - } else { - displayName = getPresentationString(presentation); - } + @Override + public void onContactInfoComplete(int callId, ContactCacheEntry entry) { + Logger.d(this, "onContactInfoComplete: ", entry.name); + Logger.d(this, "onContactInfoComplete: ", entry.number); + Logger.d(this, "onContactInfoComplete: ", entry.label); + Logger.d(this, "onContactInfoComplete: ", entry.photo); - // TODO (klp): Update secondary user call info as well. - if (isPrimary) { - updateInfoUiForPrimary(displayName, displayNumber, label); + if (mPrimary != null && mPrimary.getCallId() == callId) { + mPrimaryContactInfo = entry; + updatePrimaryDisplayInfo(); } + if (mSecondary != null && mSecondary.getCallId() == callId) { + mSecondaryContactInfo = entry; + updateSecondaryDisplayInfo(); + } + + } - // If the photoResource is filled in for the CallerInfo, (like with the - // Emergency Number case), then we can just set the photo image without - // requesting for an image load. Please refer to CallerInfoAsyncQuery.java - // for cases where CallerInfo.photoResource may be set. We can also avoid - // the image load step if the image data is cached. + private void updatePrimaryDisplayInfo() { final CallCardUi ui = getUi(); - if (info == null) return; - - // This will only be true for emergency numbers - if (info.photoResource != 0) { - ui.setImage(info.photoResource); - } else if (info.isCachedPhotoCurrent) { - if (info.cachedPhoto != null) { - ui.setImage(info.cachedPhoto); - } else { - ui.setImage(R.drawable.picture_unknown); - } - } else { - if (personUri == null) { - Logger.v(this, "personUri is null. Just use unknown picture."); - ui.setImage(R.drawable.picture_unknown); - } else if (personUri.equals(mLoadingPersonUri)) { - Logger.v(this, "The requested Uri (" + personUri + ") is being loaded already." - + " Ignore the duplicate load request."); - } else { - // Remember which person's photo is being loaded right now so that we won't issue - // unnecessary load request multiple times, which will mess up animation around - // the contact photo. - mLoadingPersonUri = personUri; - - // Load the image with a callback to update the image state. - // When the load is finished, onImageLoadComplete() will be called. - ContactsAsyncHelper.startObtainPhotoAsync(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, - mContext, personUri, this, call); - - // If the image load is too slow, we show a default avatar icon afterward. - // If it is fast enough, this message will be canceled on onImageLoadComplete(). - // TODO (klp): Figure out if this handler is still needed. - // mHandler.removeMessages(MESSAGE_SHOW_UNKNOWN_PHOTO); - // mHandler.sendEmptyMessageDelayed(MESSAGE_SHOW_UNKNOWN_PHOTO, MESSAGE_DELAY); - } + if (ui == null) { + return; } - // TODO (klp): Update other fields - photo, sip label, etc. - } - /** - * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface. - * make sure that the call state is reflected after the image is loaded. - */ - @Override - public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie) { - // mHandler.removeMessages(MESSAGE_SHOW_UNKNOWN_PHOTO); - if (mLoadingPersonUri != null) { - // Start sending view notification after the current request being done. - // New image may possibly be available from the next phone calls. - // - // TODO: may be nice to update the image view again once the newer one - // is available on contacts database. - // TODO (klp): What is this, and why does it need the write_contacts permission? - // CallerInfoUtils.sendViewNotificationAsync(mContext, mLoadingPersonUri); + if (mPrimaryContactInfo != null) { + ui.setPrimary(mPrimaryContactInfo.number, mPrimaryContactInfo.name, + mPrimaryContactInfo.label, mPrimaryContactInfo.photo); } else { - // This should not happen while we need some verbose info if it happens.. - Logger.v(this, "Person Uri isn't available while Image is successfully loaded."); + // reset to nothing (like at end of call) + ui.setPrimary(null, null, null, null); } - mLoadingPersonUri = null; - - Call call = (Call) cookie; - // TODO (klp): Handle conference calls + } + private void updateSecondaryDisplayInfo() { final CallCardUi ui = getUi(); - if (photo != null) { - ui.setImage(photo); - } else if (photoIcon != null) { - ui.setImage(photoIcon); - } else { - ui.setImage(R.drawable.picture_unknown); + if (ui == null) { + return; } - } - /** - * Updates the info portion of the call card with passed in values for the primary user. - */ - private void updateInfoUiForPrimary(String displayName, String displayNumber, String label) { - final CallCardUi ui = getUi(); - ui.setName(displayName); - ui.setNumber(displayNumber); - ui.setNumberLabel(label); + if (mSecondaryContactInfo != null) { + ui.setSecondary(true, mSecondaryContactInfo.number, mSecondaryContactInfo.name, + mSecondaryContactInfo.label, mSecondaryContactInfo.photo); + } else { + // reset to nothing so that it starts off blank next time we use it. + ui.setSecondary(false, null, null, null, null); + } } public void setAudioModeProvider(AudioModeProvider audioModeProvider) { @@ -435,13 +242,10 @@ public class CallCardPresenter extends Presenter i mAudioModeProvider.addListener(this); } - public String getPresentationString(int presentation) { - String name = mContext.getString(R.string.unknown); - if (presentation == Call.PRESENTATION_RESTRICTED) { - name = mContext.getString(R.string.private_num); - } else if (presentation == Call.PRESENTATION_PAYPHONE) { - name = mContext.getString(R.string.payphone); - } - return name; + public interface CallCardUi extends Ui { + void setVisible(boolean on); + void setPrimary(String number, String name, String label, Drawable photo); + void setSecondary(boolean show, String number, String name, String label, Drawable photo); + void setCallState(int state, Call.DisconnectCause cause, boolean bluetoothOn); } } diff --git a/InCallUI/src/com/android/incallui/CallList.java b/InCallUI/src/com/android/incallui/CallList.java index 3041fa7fc..eb05cdb04 100644 --- a/InCallUI/src/com/android/incallui/CallList.java +++ b/InCallUI/src/com/android/incallui/CallList.java @@ -221,7 +221,6 @@ public class CallList { } } - Logger.v(this, "Found call: ", retval); return retval; } diff --git a/InCallUI/src/com/android/incallui/CallerInfoUtils.java b/InCallUI/src/com/android/incallui/CallerInfoUtils.java index 077502bcd..845a6e3f9 100644 --- a/InCallUI/src/com/android/incallui/CallerInfoUtils.java +++ b/InCallUI/src/com/android/incallui/CallerInfoUtils.java @@ -24,13 +24,12 @@ public class CallerInfoUtils { private static final int QUERY_TOKEN = -1; /** - * This is called to get caller info for a call. For outgoing calls, uri should not be null - * because we know which contact uri the user selected to make the outgoing call. This - * will return a CallerInfo object immediately based off information in the call, but + * This is called to get caller info for a call. This will return a CallerInfo + * object immediately based off information in the call, but * more information is returned to the OnQueryCompleteListener (which contains * information about the phone number label, user's name, etc). */ - public static CallerInfo getCallerInfoForCall(Context context, Call call, Uri uri, + public static CallerInfo getCallerInfoForCall(Context context, Call call, CallerInfoAsyncQuery.OnQueryCompleteListener listener) { CallerInfo info = new CallerInfo(); String number = call.getNumber(); @@ -42,29 +41,26 @@ public class CallerInfoUtils { info.numberPresentation = call.getNumberPresentation(); info.namePresentation = call.getCnapNamePresentation(); - if (uri != null) { - // Have an URI, so pass it to startQuery - CallerInfoAsyncQuery.startQuery(QUERY_TOKEN, context, uri, listener, call); - } else { - if (!TextUtils.isEmpty(number)) { - number = modifyForSpecialCnapCases(context, info, number, info.numberPresentation); - info.phoneNumber = number; - - // For scenarios where we may receive a valid number from the network but a - // restricted/unavailable presentation, we do not want to perform a contact query, - // so just return the existing caller info. - if (info.numberPresentation != Call.PRESENTATION_ALLOWED) { - return info; - } else { - // Start the query with the number provided from the call. - Logger.d(TAG, "==> Actually starting CallerInfoAsyncQuery.startQuery()..."); - CallerInfoAsyncQuery.startQuery(QUERY_TOKEN, context, number, listener, call); - } - } else { - // The number is null or empty (Blocked caller id or empty). Just return the - // caller info object as is, without starting a query. + // TODO: Have phoneapp send a Uri when it knows the contact that triggered this call. + + if (!TextUtils.isEmpty(number)) { + number = modifyForSpecialCnapCases(context, info, number, info.numberPresentation); + info.phoneNumber = number; + + // For scenarios where we may receive a valid number from the network but a + // restricted/unavailable presentation, we do not want to perform a contact query, + // so just return the existing caller info. + if (info.numberPresentation != Call.PRESENTATION_ALLOWED) { return info; + } else { + // Start the query with the number provided from the call. + Logger.d(TAG, "==> Actually starting CallerInfoAsyncQuery.startQuery()..."); + CallerInfoAsyncQuery.startQuery(QUERY_TOKEN, context, number, listener, call); } + } else { + // The number is null or empty (Blocked caller id or empty). Just return the + // caller info object as is, without starting a query. + return info; } return info; diff --git a/InCallUI/src/com/android/incallui/ContactInfoCache.java b/InCallUI/src/com/android/incallui/ContactInfoCache.java new file mode 100644 index 000000000..6cbe060ac --- /dev/null +++ b/InCallUI/src/com/android/incallui/ContactInfoCache.java @@ -0,0 +1,375 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.incallui; + +import com.google.android.collect.Lists; +import com.google.android.collect.Maps; +import com.google.common.base.Preconditions; + +import android.content.ContentUris; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Looper; +import android.provider.ContactsContract.Contacts; +import android.text.TextUtils; + +import com.android.services.telephony.common.Call; + +import java.util.List; +import java.util.Map; + +/** + * Class responsible for querying Contact Information for Call objects. + * Can perform asynchronous requests to the Contact Provider for information as well + * as respond synchronously for any data that it currently has cached from previous + * queries. + * This class always gets called from the UI thread so it does not need thread protection. + */ +public class ContactInfoCache implements CallerInfoAsyncQuery.OnQueryCompleteListener, + ContactsAsyncHelper.OnImageLoadCompleteListener { + + private static final int TOKEN_UPDATE_PHOTO_FOR_CALL_STATE = 0; + + private final Context mContext; + private final Map mInfoMap = Maps.newHashMap(); + + public ContactInfoCache(Context context) { + mContext = context; + } + + /** + * Requests contact data for the Call object passed in. + * Returns the data through callback. If callback is null, no response is made, however the + * query is still performed and cached. + * + * @param call The call to look up. + * @param callback The function to call back when the call is found. Can be null. + */ + public void findInfo(Call call, ContactInfoCacheCallback callback) { + Preconditions.checkState(Looper.getMainLooper().getThread() == Thread.currentThread()); + Preconditions.checkNotNull(callback); + Preconditions.checkNotNull(call); + + final SearchEntry entry; + + // If the entry already exists, add callback + if (mInfoMap.containsKey(call.getCallId())) { + entry = mInfoMap.get(call.getCallId()); + + // If this entry is still pending, the callback will also get called when it returns. + if (!entry.finished) { + entry.addCallback(callback); + } + } else { + entry = new SearchEntry(call, callback); + mInfoMap.put(call.getCallId(), entry); + startQuery(entry); + } + + // Call back with the information we have + callback.onContactInfoComplete(entry.call.getCallId(), entry.info); + } + + /** + * Callback method for asynchronous caller information query. + */ + @Override + public void onQueryComplete(int token, Object cookie, CallerInfo ci) { + if (cookie instanceof Call) { + final Call call = (Call) cookie; + + if (!mInfoMap.containsKey(call.getCallId())) { + return; + } + + final SearchEntry entry = mInfoMap.get(call.getCallId()); + + int presentationMode = call.getNumberPresentation(); + if (ci.contactExists || ci.isEmergencyNumber() || ci.isVoiceMailNumber()) { + presentationMode = Call.PRESENTATION_ALLOWED; + } + + // start photo query + + updateCallerInfo(entry, ci, presentationMode); + } + } + + /** + * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface. + * make sure that the call state is reflected after the image is loaded. + */ + @Override + public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie) { + Logger.d(this, "Image load complete with context: ", mContext); + // TODO: may be nice to update the image view again once the newer one + // is available on contacts database. + // TODO (klp): What is this, and why does it need the write_contacts permission? + // CallerInfoUtils.sendViewNotificationAsync(mContext, mLoadingPersonUri); + + final Call call = (Call) cookie; + + if (!mInfoMap.containsKey(call.getCallId())) { + Logger.e(this, "Image Load received for empty search entry."); + return; + } + + final SearchEntry entry = mInfoMap.get(call.getCallId()); + + Logger.d(this, "setting photo for entry: ", entry); + + // TODO (klp): Handle conference calls + if (photo != null) { + Logger.v(this, "direct drawable: ", photo); + entry.info.photo = photo; + } else if (photoIcon != null) { + Logger.v(this, "photo icon: ", photoIcon); + entry.info.photo = new BitmapDrawable(mContext.getResources(), photoIcon); + } else { + Logger.v(this, "unknown photo"); + entry.info.photo = mContext.getResources().getDrawable(R.drawable.picture_unknown); + } + + sendNotification(entry); + } + + /** + * Performs a query for caller information. + * Save any immediate data we get from the query. An asynchronous query may also be made + * for any data that we do not already have. Some queries, such as those for voicemail and + * emergency call information, will not perform an additional asynchronous query. + */ + private void startQuery(SearchEntry entry) { + final CallerInfo ci = CallerInfoUtils.getCallerInfoForCall(mContext, entry.call, this); + + updateCallerInfo(entry, ci, entry.call.getNumberPresentation()); + } + + private void updateCallerInfo(SearchEntry entry, CallerInfo info, int presentation) { + // The actual strings we're going to display onscreen: + String displayName; + String displayNumber = null; + String label = null; + Uri personUri = null; + Drawable photo = null; + + final Call call = entry.call; + + // Gather missing info unless the call is generic, in which case we wouldn't use + // the gathered information anyway. + if (info != null) { + + // It appears that there is a small change in behaviour with the + // PhoneUtils' startGetCallerInfo whereby if we query with an + // empty number, we will get a valid CallerInfo object, but with + // fields that are all null, and the isTemporary boolean input + // parameter as true. + + // In the past, we would see a NULL callerinfo object, but this + // ends up causing null pointer exceptions elsewhere down the + // line in other cases, so we need to make this fix instead. It + // appears that this was the ONLY call to PhoneUtils + // .getCallerInfo() that relied on a NULL CallerInfo to indicate + // an unknown contact. + + // Currently, infi.phoneNumber may actually be a SIP address, and + // if so, it might sometimes include the "sip:" prefix. That + // prefix isn't really useful to the user, though, so strip it off + // if present. (For any other URI scheme, though, leave the + // prefix alone.) + // TODO: It would be cleaner for CallerInfo to explicitly support + // SIP addresses instead of overloading the "phoneNumber" field. + // Then we could remove this hack, and instead ask the CallerInfo + // for a "user visible" form of the SIP address. + String number = info.phoneNumber; + if ((number != null) && number.startsWith("sip:")) { + number = number.substring(4); + } + + if (TextUtils.isEmpty(info.name)) { + // No valid "name" in the CallerInfo, so fall back to + // something else. + // (Typically, we promote the phone number up to the "name" slot + // onscreen, and possibly display a descriptive string in the + // "number" slot.) + if (TextUtils.isEmpty(number)) { + // No name *or* number! Display a generic "unknown" string + // (or potentially some other default based on the presentation.) + displayName = getPresentationString(presentation); + Logger.d(this, " ==> no name *or* number! displayName = " + displayName); + } else if (presentation != Call.PRESENTATION_ALLOWED) { + // This case should never happen since the network should never send a phone # + // AND a restricted presentation. However we leave it here in case of weird + // network behavior + displayName = getPresentationString(presentation); + Logger.d(this, " ==> presentation not allowed! displayName = " + displayName); + } else if (!TextUtils.isEmpty(info.cnapName)) { + // No name, but we do have a valid CNAP name, so use that. + displayName = info.cnapName; + info.name = info.cnapName; + displayNumber = number; + Logger.d(this, " ==> cnapName available: displayName '" + + displayName + "', displayNumber '" + displayNumber + "'"); + } else { + // No name; all we have is a number. This is the typical + // case when an incoming call doesn't match any contact, + // or if you manually dial an outgoing number using the + // dialpad. + + // Promote the phone number up to the "name" slot: + displayName = number; + + // ...and use the "number" slot for a geographical description + // string if available (but only for incoming calls.) + if ((call != null) && (call.getState() == Call.State.INCOMING)) { + // TODO (CallerInfoAsyncQuery cleanup): Fix the CallerInfo + // query to only do the geoDescription lookup in the first + // place for incoming calls. + displayNumber = info.geoDescription; // may be null + Logger.d(this, "Geodescrption: " + info.geoDescription); + } + + Logger.d(this, " ==> no name; falling back to number: displayName '" + + displayName + "', displayNumber '" + displayNumber + "'"); + } + } else { + // We do have a valid "name" in the CallerInfo. Display that + // in the "name" slot, and the phone number in the "number" slot. + if (presentation != Call.PRESENTATION_ALLOWED) { + // This case should never happen since the network should never send a name + // AND a restricted presentation. However we leave it here in case of weird + // network behavior + displayName = getPresentationString(presentation); + Logger.d(this, " ==> valid name, but presentation not allowed!" + + " displayName = " + displayName); + } else { + displayName = info.name; + displayNumber = number; + label = info.phoneLabel; + Logger.d(this, " ==> name is present in CallerInfo: displayName '" + + displayName + "', displayNumber '" + displayNumber + "'"); + } + } + personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, info.person_id); + Logger.d(this, "- got personUri: '" + personUri + + "', based on info.person_id: " + info.person_id); + } else { + displayName = getPresentationString(presentation); + } + + // This will only be true for emergency numbers + if (info.photoResource != 0) { + photo = mContext.getResources().getDrawable(info.photoResource); + } else if (info.isCachedPhotoCurrent) { + if (info.cachedPhoto != null) { + photo = info.cachedPhoto; + } else { + photo = mContext.getResources().getDrawable(R.drawable.picture_unknown); + } + } else { + if (personUri == null) { + Logger.v(this, "personUri is null. Just use unknown picture."); + photo = mContext.getResources().getDrawable(R.drawable.picture_unknown); + } else { + Logger.d(this, "startObtainPhotoAsync"); + // Load the image with a callback to update the image state. + // When the load is finished, onImageLoadComplete() will be called. + ContactsAsyncHelper.startObtainPhotoAsync(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, + mContext, personUri, this, entry.call); + + // If the image load is too slow, we show a default avatar icon afterward. + // If it is fast enough, this message will be canceled on onImageLoadComplete(). + // TODO (klp): Figure out if this handler is still needed. + // mHandler.removeMessages(MESSAGE_SHOW_UNKNOWN_PHOTO); + // mHandler.sendEmptyMessageDelayed(MESSAGE_SHOW_UNKNOWN_PHOTO, MESSAGE_DELAY); + } + } + + final ContactCacheEntry cce = entry.info; + cce.name = displayName; + cce.number = displayNumber; + cce.label = label; + cce.photo = photo; + + sendNotification(entry); + } + + /** + * Sends the updated information to call the callbacks for the entry. + */ + private void sendNotification(SearchEntry entry) { + for (int i = 0; i < entry.callbacks.size(); i++) { + entry.callbacks.get(i).onContactInfoComplete(entry.call.getCallId(), entry.info); + } + } + + /** + * Gets name strings based on some special presentation modes. + */ + private String getPresentationString(int presentation) { + String name = mContext.getString(R.string.unknown); + if (presentation == Call.PRESENTATION_RESTRICTED) { + name = mContext.getString(R.string.private_num); + } else if (presentation == Call.PRESENTATION_PAYPHONE) { + name = mContext.getString(R.string.payphone); + } + return name; + } + + /** + * Callback interface for the contact query. + */ + public interface ContactInfoCacheCallback { + public void onContactInfoComplete(int callId, ContactCacheEntry entry); + } + + public static class ContactCacheEntry { + public String name; + public String number; + public String label; + public Drawable photo; + } + + private static class SearchEntry { + public Call call; + public boolean finished; + public final ContactCacheEntry info; + public final List callbacks = Lists.newArrayList(); + + public SearchEntry(Call call, ContactInfoCacheCallback callback) { + this.call = call; + + info = new ContactCacheEntry(); + finished = false; + callbacks.add(callback); + } + + public void addCallback(ContactInfoCacheCallback cb) { + if (!callbacks.contains(cb)) { + callbacks.add(cb); + } + } + + public void finish() { + callbacks.clear(); + finished = true; + } + } +} diff --git a/InCallUI/src/com/android/incallui/ContactsAsyncHelper.java b/InCallUI/src/com/android/incallui/ContactsAsyncHelper.java index c9a331771..305486fd1 100644 --- a/InCallUI/src/com/android/incallui/ContactsAsyncHelper.java +++ b/InCallUI/src/com/android/incallui/ContactsAsyncHelper.java @@ -38,9 +38,6 @@ import java.io.InputStream; */ public class ContactsAsyncHelper { - private static final boolean DBG = false; - private static final String LOG_TAG = "ContactsAsyncHelper"; - /** * Interface for a WorkerHandler result return. */ @@ -71,10 +68,8 @@ public class ContactsAsyncHelper { switch (msg.arg1) { case EVENT_LOAD_IMAGE: if (args.listener != null) { - if (DBG) { - Log.d(LOG_TAG, "Notifying listener: " + args.listener.toString() + - " image: " + args.uri + " completed"); - } + Logger.d(this, "Notifying listener: " + args.listener.toString() + + " image: " + args.uri + " completed"); args.listener.onImageLoadComplete(msg.what, args.photo, args.photoIcon, args.cookie); } @@ -197,7 +192,7 @@ public class ContactsAsyncHelper { inputStream = Contacts.openContactPhotoInputStream( args.context.getContentResolver(), args.uri, true); } catch (Exception e) { - Log.e(LOG_TAG, "Error opening photo input stream", e); + Logger.e(this, "Error opening photo input stream", e); } if (inputStream != null) { @@ -208,25 +203,21 @@ public class ContactsAsyncHelper { // BitmapDrawable and thus we can have (down)scaled version of it. args.photoIcon = getPhotoIconWhenAppropriate(args.context, args.photo); - if (DBG) { - Log.d(LOG_TAG, "Loading image: " + msg.arg1 + - " token: " + msg.what + " image URI: " + args.uri); - } + Logger.d(ContactsAsyncHelper.this, "Loading image: " + msg.arg1 + + " token: " + msg.what + " image URI: " + args.uri); } else { args.photo = null; args.photoIcon = null; - if (DBG) { - Log.d(LOG_TAG, "Problem with image: " + msg.arg1 + - " token: " + msg.what + " image URI: " + args.uri + - ", using default image."); - } + Logger.d(ContactsAsyncHelper.this, "Problem with image: " + msg.arg1 + + " token: " + msg.what + " image URI: " + args.uri + + ", using default image."); } } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { - Log.e(LOG_TAG, "Unable to close input stream.", e); + Logger.e(this, "Unable to close input stream.", e); } } } @@ -264,7 +255,7 @@ public class ContactsAsyncHelper { // If the longer edge is much longer than the shorter edge, the latter may // become 0 which will cause a crash. if (newWidth <= 0 || newHeight <= 0) { - Log.w(LOG_TAG, "Photo icon's width or height become 0."); + Logger.w(this, "Photo icon's width or height become 0."); return null; } @@ -307,7 +298,7 @@ public class ContactsAsyncHelper { // in case the source caller info is null, the URI will be null as well. // just update using the placeholder image in this case. if (personUri == null) { - Log.wtf(LOG_TAG, "Uri is missing"); + Logger.wtf("startObjectPhotoAsync", "Uri is missing"); return; } @@ -326,7 +317,7 @@ public class ContactsAsyncHelper { msg.arg1 = EVENT_LOAD_IMAGE; msg.obj = args; - if (DBG) Log.d(LOG_TAG, "Begin loading image: " + args.uri + + Logger.d("startObjectPhotoAsync", "Begin loading image: " + args.uri + ", displaying default image for now."); // notify the thread to begin working diff --git a/InCallUI/src/com/android/incallui/Logger.java b/InCallUI/src/com/android/incallui/Logger.java index e7cbe2025..10433bee6 100644 --- a/InCallUI/src/com/android/incallui/Logger.java +++ b/InCallUI/src/com/android/incallui/Logger.java @@ -53,6 +53,12 @@ import android.util.Log; } } + public static void v(Object obj, String str1, Object str2) { + if (VERBOSE) { + Log.d(TAG, getPrefix(obj) + str1 + str2); + } + } + public static void e(String tag, String msg, Exception e) { Log.e(TAG, tag + msg, e); } @@ -61,12 +67,6 @@ import android.util.Log; Log.e(TAG, tag + msg); } - public static void v(Object obj, String str1, Object str2) { - if (VERBOSE) { - Log.d(TAG, getPrefix(obj) + str1 + str2); - } - } - public static void e(Object obj, String msg, Exception e) { Log.e(TAG, getPrefix(obj) + msg, e); } @@ -83,6 +83,10 @@ import android.util.Log; Log.i(TAG, getPrefix(obj) + msg); } + public static void w(Object obj, String msg) { + Log.w(TAG, getPrefix(obj) + msg); + } + public static void wtf(Object obj, String msg) { Log.wtf(TAG, getPrefix(obj) + msg); } -- cgit v1.2.3