summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSantos Cordon <santoscordon@google.com>2013-08-12 16:31:35 -0700
committerSantos Cordon <santoscordon@google.com>2013-08-13 10:09:20 -0700
commitd2bd93fe3929403f84367f826f07cbadc0c6366b (patch)
treefda1b15279840e8f1665b5e12ad219ea1d3b8b99
parentb8eedb3b9796873eaa22847205239a51092f1a43 (diff)
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
-rw-r--r--InCallUI/src/com/android/incallui/CallCardFragment.java120
-rw-r--r--InCallUI/src/com/android/incallui/CallCardPresenter.java358
-rw-r--r--InCallUI/src/com/android/incallui/CallList.java1
-rw-r--r--InCallUI/src/com/android/incallui/CallerInfoUtils.java46
-rw-r--r--InCallUI/src/com/android/incallui/ContactInfoCache.java375
-rw-r--r--InCallUI/src/com/android/incallui/ContactsAsyncHelper.java33
-rw-r--r--InCallUI/src/com/android/incallui/Logger.java16
7 files changed, 563 insertions, 386 deletions
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<CallCardPresenter>
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<CallCardPresenter>
}
@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<CallCardPresenter>
}
}
+ 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<CallCardPresenter>
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<CallCardPresenter.CallCardUi> implements
- InCallStateListener, CallerInfoAsyncQuery.OnQueryCompleteListener,
- ContactsAsyncHelper.OnImageLoadCompleteListener, AudioModeListener {
-
- private static final int TOKEN_UPDATE_PHOTO_FOR_CALL_STATE = 0;
+public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi>
+ 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<CallCardPresenter.CallCardUi> 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<CallCardPresenter.CallCardUi> 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
@@ -149,6 +132,25 @@ public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi> i
}
/**
+ * 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
* to display to the user. Callers can use the "ignore" feature to get the second best call
@@ -183,251 +185,56 @@ public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi> 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<CallCardPresenter.CallCardUi> 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<Integer, SearchEntry> 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<ContactInfoCacheCallback> 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);
}