Merge "Set privileged to false." into klp-dev
diff --git a/InCallUI/res/drawable-hdpi/stat_sys_phone_call_on_hold.png b/InCallUI/res/drawable-hdpi/stat_sys_phone_call_on_hold.png
new file mode 100644
index 0000000..7e7bc3e
--- /dev/null
+++ b/InCallUI/res/drawable-hdpi/stat_sys_phone_call_on_hold.png
Binary files differ
diff --git a/InCallUI/res/drawable-ldrtl-hdpi/stat_sys_phone_call_on_hold.png b/InCallUI/res/drawable-ldrtl-hdpi/stat_sys_phone_call_on_hold.png
new file mode 100644
index 0000000..18de248
--- /dev/null
+++ b/InCallUI/res/drawable-ldrtl-hdpi/stat_sys_phone_call_on_hold.png
Binary files differ
diff --git a/InCallUI/res/drawable-ldrtl-mdpi/stat_sys_phone_call_on_hold.png b/InCallUI/res/drawable-ldrtl-mdpi/stat_sys_phone_call_on_hold.png
new file mode 100644
index 0000000..60f665d
--- /dev/null
+++ b/InCallUI/res/drawable-ldrtl-mdpi/stat_sys_phone_call_on_hold.png
Binary files differ
diff --git a/InCallUI/res/drawable-ldrtl-xhdpi/stat_sys_phone_call_on_hold.png b/InCallUI/res/drawable-ldrtl-xhdpi/stat_sys_phone_call_on_hold.png
new file mode 100644
index 0000000..2cbcb5f
--- /dev/null
+++ b/InCallUI/res/drawable-ldrtl-xhdpi/stat_sys_phone_call_on_hold.png
Binary files differ
diff --git a/InCallUI/res/drawable-mdpi/stat_sys_phone_call_on_hold.png b/InCallUI/res/drawable-mdpi/stat_sys_phone_call_on_hold.png
new file mode 100644
index 0000000..20ff4b6
--- /dev/null
+++ b/InCallUI/res/drawable-mdpi/stat_sys_phone_call_on_hold.png
Binary files differ
diff --git a/InCallUI/res/drawable-xhdpi/stat_sys_phone_call_on_hold.png b/InCallUI/res/drawable-xhdpi/stat_sys_phone_call_on_hold.png
new file mode 100644
index 0000000..a6748d8
--- /dev/null
+++ b/InCallUI/res/drawable-xhdpi/stat_sys_phone_call_on_hold.png
Binary files differ
diff --git a/InCallUI/src/com/android/incallui/CallCardFragment.java b/InCallUI/src/com/android/incallui/CallCardFragment.java
index b4fc852..552a42a 100644
--- a/InCallUI/src/com/android/incallui/CallCardFragment.java
+++ b/InCallUI/src/com/android/incallui/CallCardFragment.java
@@ -46,6 +46,7 @@
private TextView mCallStateLabel;
private ViewStub mSecondaryCallInfo;
private TextView mSecondaryCallName;
+ private ImageView mSecondaryPhoto;
// Cached DisplayMetrics density.
private float mDensity;
@@ -94,59 +95,67 @@
}
@Override
- public void onAttach(Activity activity) {
- super.onAttach(activity);
- getPresenter().setContext(activity);
- }
+ public void setPrimary(String number, String name, String label, Drawable photo) {
+ boolean nameIsNumber = false;
- @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);
+ // 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);
+
+ int nameDirection = View.TEXT_DIRECTION_INHERIT;
+ if (nameIsNumber) {
+ nameDirection = View.TEXT_DIRECTION_LTR;
+ }
+ mName.setTextDirection(nameDirection);
}
- }
- @Override
- public void setName(String name) {
- setName(name, false);
- }
-
- @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 +189,21 @@
}
}
+ private void setDrawableToImageView(ImageView view, Drawable photo) {
+ if (photo == null) {
+ photo = view.getResources().getDrawable(R.drawable.picture_unknown);
+ }
+
+ 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 +347,8 @@
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 d4a39ea..8a855c1 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,17 @@
* 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 {
+public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi>
+ implements InCallStateListener, AudioModeListener, ContactInfoCacheCallback {
- private static final int TOKEN_UPDATE_PHOTO_FOR_CALL_STATE = 0;
-
- 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 +62,13 @@
mAudioModeProvider.removeListener(this);
}
mPrimary = null;
+ mPrimaryContactInfo = null;
+ mSecondaryContactInfo = null;
}
- public void setContext(Context context) {
- mContext = context;
+ public void setContactInfoCache(ContactInfoCache cache) {
+ mContactInfoCache = cache;
+ startContactInfoSearch();
}
@Override
@@ -107,32 +97,23 @@
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 +130,25 @@
}
/**
+ * 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 +183,56 @@
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.
- */
- 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);
- }
-
- // TODO (klp): Update secondary user call info as well.
- if (isPrimary) {
- updateInfoUiForPrimary(displayName, displayNumber, label);
- }
-
- // 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.
- 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);
- }
- }
- // 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.
+ * Callback received when Contact info data query completes.
*/
@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);
- } 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.");
+ 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);
+
+ if (mPrimary != null && mPrimary.getCallId() == callId) {
+ mPrimaryContactInfo = entry;
+ updatePrimaryDisplayInfo();
}
- mLoadingPersonUri = null;
-
- Call call = (Call) cookie;
-
- // TODO (klp): Handle conference calls
-
- 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 (mSecondary != null && mSecondary.getCallId() == callId) {
+ mSecondaryContactInfo = entry;
+ updateSecondaryDisplayInfo();
}
+
}
- /**
- * 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) {
+ private void updatePrimaryDisplayInfo() {
final CallCardUi ui = getUi();
- ui.setName(displayName);
- ui.setNumber(displayNumber);
- ui.setNumberLabel(label);
+ if (ui == null) {
+ return;
+ }
+
+ if (mPrimaryContactInfo != null) {
+ ui.setPrimary(mPrimaryContactInfo.number, mPrimaryContactInfo.name,
+ mPrimaryContactInfo.label, mPrimaryContactInfo.photo);
+ } else {
+ // reset to nothing (like at end of call)
+ ui.setPrimary(null, null, null, null);
+ }
+
+ }
+
+ private void updateSecondaryDisplayInfo() {
+ final CallCardUi ui = getUi();
+ if (ui == null) {
+ return;
+ }
+
+ 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 +240,10 @@
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 3041fa7..eb05cdb 100644
--- a/InCallUI/src/com/android/incallui/CallList.java
+++ b/InCallUI/src/com/android/incallui/CallList.java
@@ -221,7 +221,6 @@
}
}
- 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 077502b..845a6e3 100644
--- a/InCallUI/src/com/android/incallui/CallerInfoUtils.java
+++ b/InCallUI/src/com/android/incallui/CallerInfoUtils.java
@@ -24,13 +24,12 @@
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 @@
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;
+ // TODO: Have phoneapp send a Uri when it knows the contact that triggered this call.
- // 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.
+ 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 0000000..c5a4cf5
--- /dev/null
+++ b/InCallUI/src/com/android/incallui/ContactInfoCache.java
@@ -0,0 +1,382 @@
+/*
+ * 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 = null;
+ }
+
+ sendNotification(entry);
+ }
+
+ /**
+ * Blows away the stored cache values.
+ */
+ public void clearCache() {
+ mInfoMap.clear();
+ }
+
+ /**
+ * 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 c9a3317..305486f 100644
--- a/InCallUI/src/com/android/incallui/ContactsAsyncHelper.java
+++ b/InCallUI/src/com/android/incallui/ContactsAsyncHelper.java
@@ -38,9 +38,6 @@
*/
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 @@
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 @@
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 @@
// 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 @@
// 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 @@
// 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 @@
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/InCallActivity.java b/InCallUI/src/com/android/incallui/InCallActivity.java
index 8ae37bd..e1a9796 100644
--- a/InCallUI/src/com/android/incallui/InCallActivity.java
+++ b/InCallUI/src/com/android/incallui/InCallActivity.java
@@ -34,6 +34,7 @@
private CallCardFragment mCallCardFragment;
private AnswerFragment mAnswerFragment;
private DialpadFragment mDialpadFragment;
+ private boolean mIsForegroundActivity;
@Override
protected void onCreate(Bundle icicle) {
@@ -62,6 +63,9 @@
protected void onResume() {
Logger.d(this, "onResume()...");
super.onResume();
+
+ mIsForegroundActivity = true;
+ InCallPresenter.getInstance().onUiShowing(true);
}
// onPause is guaranteed to be called when the InCallActivity goes
@@ -70,6 +74,9 @@
protected void onPause() {
Logger.d(this, "onPause()...");
super.onPause();
+
+ mIsForegroundActivity = false;
+ InCallPresenter.getInstance().onUiShowing(false);
}
@Override
@@ -85,6 +92,13 @@
}
/**
+ * Returns true when theActivity is in foreground (between onResume and onPause).
+ */
+ /* package */ boolean isForegroundActivity() {
+ return mIsForegroundActivity;
+ }
+
+ /**
* Dismisses the in-call screen.
*
* We never *really* finish() the InCallActivity, since we don't want to get destroyed and then
@@ -273,6 +287,8 @@
mainPresenter.getAudioModeProvider());
mCallCardFragment.getPresenter().setAudioModeProvider(
mainPresenter.getAudioModeProvider());
+ mCallCardFragment.getPresenter().setContactInfoCache(
+ mainPresenter.getContactInfoCache());
mainPresenter.addListener(mCallButtonFragment.getPresenter());
mainPresenter.addListener(mCallCardFragment.getPresenter());
diff --git a/InCallUI/src/com/android/incallui/InCallPresenter.java b/InCallUI/src/com/android/incallui/InCallPresenter.java
index c56d980..3ae4d3a 100644
--- a/InCallUI/src/com/android/incallui/InCallPresenter.java
+++ b/InCallUI/src/com/android/incallui/InCallPresenter.java
@@ -43,6 +43,7 @@
private AudioModeProvider mAudioModeProvider;
private StatusBarNotifier mStatusBarNotifier;
+ private ContactInfoCache mContactInfoCache;
private Context mContext;
private CallList mCallList;
private InCallActivity mInCallActivity;
@@ -63,7 +64,9 @@
mCallList = callList;
mCallList.addListener(this);
- mStatusBarNotifier = new StatusBarNotifier(context);
+ mContactInfoCache = new ContactInfoCache(context);
+
+ mStatusBarNotifier = new StatusBarNotifier(context, mContactInfoCache, mCallList);
addListener(mStatusBarNotifier);
mAudioModeProvider = audioModeProvider;
@@ -148,6 +151,10 @@
return mAudioModeProvider;
}
+ public ContactInfoCache getContactInfoCache() {
+ return mContactInfoCache;
+ }
+
/**
* Hangs up any active or outgoing calls.
*/
@@ -163,6 +170,23 @@
}
/**
+ * Returns true if the incall app is the foreground application.
+ */
+ public boolean isShowingInCallUi() {
+ return (mInCallActivity != null &&
+ mInCallActivity.isForegroundActivity());
+ }
+
+ /**
+ * Called when the activity goes out of the foreground.
+ */
+ public void onUiShowing(boolean showing) {
+ // We need to update the notification bar when we leave the UI because that
+ // could trigger it to show again.
+ mStatusBarNotifier.updateNotification(mInCallState, mCallList);
+ }
+
+ /**
* When the state of in-call changes, this is the first method to get called. It determines if
* the UI needs to be started or finished depending on the new state and does it.
*/
@@ -215,7 +239,7 @@
if (startStartupSequence) {
- mStatusBarNotifier.updateNotificationAndLaunchIncomingCallUi(newState);
+ mStatusBarNotifier.updateNotificationAndLaunchIncomingCallUi(newState, mCallList);
} else if (showCallUi) {
showInCall();
} else if (newState == InCallState.HIDDEN) {
@@ -227,6 +251,10 @@
mInCallActivity = null;
temp.finish();
+
+ // blow away stale contact info so that we get fresh data on
+ // the next set of calls
+ mContactInfoCache.clearCache();
}
}
diff --git a/InCallUI/src/com/android/incallui/Logger.java b/InCallUI/src/com/android/incallui/Logger.java
index e7cbe20..10433be 100644
--- a/InCallUI/src/com/android/incallui/Logger.java
+++ b/InCallUI/src/com/android/incallui/Logger.java
@@ -53,6 +53,12 @@
}
}
+ 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 @@
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 @@
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);
}
diff --git a/InCallUI/src/com/android/incallui/StatusBarNotifier.java b/InCallUI/src/com/android/incallui/StatusBarNotifier.java
index 857b6ce..e2dc42c 100644
--- a/InCallUI/src/com/android/incallui/StatusBarNotifier.java
+++ b/InCallUI/src/com/android/incallui/StatusBarNotifier.java
@@ -23,7 +23,13 @@
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import com.android.incallui.ContactInfoCache.ContactCacheEntry;
+import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
import com.android.incallui.InCallApp.NotificationBroadcastReceiver;
import com.android.incallui.InCallPresenter.InCallState;
import com.android.services.telephony.common.Call;
@@ -31,18 +37,29 @@
/**
* This class adds Notifications to the status bar for the in-call experience.
*/
-public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
+public class StatusBarNotifier implements InCallPresenter.InCallStateListener,
+ ContactInfoCacheCallback {
// notification types
private static final int IN_CALL_NOTIFICATION = 1;
private final Context mContext;
+ private final ContactInfoCache mContactInfoCache;
+ private final CallList mCallList;
private final NotificationManager mNotificationManager;
+ private boolean mIsShowingNotification = false;
private InCallState mInCallState = InCallState.HIDDEN;
+ private int mSavedIcon = 0;
+ private int mSavedContent = 0;
+ private Bitmap mSavedLargeIcon;
+ private String mSavedContentTitle;
- public StatusBarNotifier(Context context) {
+ public StatusBarNotifier(Context context, ContactInfoCache contactInfoCache,
+ CallList callList) {
Preconditions.checkNotNull(context);
mContext = context;
+ mContactInfoCache = contactInfoCache;
+ mCallList = callList;
mNotificationManager =
(NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
}
@@ -52,7 +69,15 @@
*/
@Override
public void onStateChange(InCallState state, CallList callList) {
- updateInCallNotification(state);
+ updateNotification(state, callList);
+ }
+
+ /**
+ * Called after the Contact Info query has finished.
+ */
+ @Override
+ public void onContactInfoComplete(int callId, ContactCacheEntry entry) {
+ updateNotification(mInCallState, mCallList);
}
/**
@@ -63,10 +88,10 @@
* This method will never actually launch the incoming-call UI.
* (Use updateNotificationAndLaunchIncomingCallUi() for that.)
*/
- private void updateInCallNotification(InCallState state) {
+ public void updateNotification(InCallState state, CallList callList) {
// allowFullScreenIntent=false means *don't* allow the incoming
// call UI to be launched.
- updateInCallNotification(false, state);
+ updateInCallNotification(false, state, callList);
}
/**
@@ -100,10 +125,10 @@
*
* @see #updateInCallNotification(boolean)
*/
- public void updateNotificationAndLaunchIncomingCallUi(InCallState state) {
+ public void updateNotificationAndLaunchIncomingCallUi(InCallState state, CallList callList) {
// Set allowFullScreenIntent=true to indicate that we *should*
// launch the incoming call UI if necessary.
- updateInCallNotification(true, state);
+ updateInCallNotification(true, state, callList);
}
@@ -114,6 +139,8 @@
private void cancelInCall() {
Logger.d(this, "cancelInCall()...");
mNotificationManager.cancel(IN_CALL_NOTIFICATION);
+
+ mIsShowingNotification = false;
}
/**
@@ -129,37 +156,76 @@
* Watch out: This should be set to true *only* when directly
* handling a new incoming call for the first time.
*/
- private void updateInCallNotification(boolean allowFullScreenIntent, InCallState state) {
+ private void updateInCallNotification(final boolean allowFullScreenIntent,
+ final InCallState state, CallList callList) {
Logger.d(this, "updateInCallNotification(allowFullScreenIntent = "
+ allowFullScreenIntent + ")...");
- // First, we dont need to continue issuing new notifications if the state hasn't
- // changed from the last time we did this.
- if (mInCallState == state) {
- return;
- }
- mInCallState = state;
-
- if (!state.isConnectingOrConnected()) {
+ if (shouldSuppressNotification(state, callList)) {
cancelInCall();
return;
}
- final PendingIntent inCallPendingIntent = createLaunchPendingIntent();
- final Notification.Builder builder = getNotificationBuilder();
- builder.setContentIntent(inCallPendingIntent);
+ final Call call = getCallToShow(callList);
+ if (call == null) {
+ Logger.wtf(this, "No call for the notification!");
+ }
+
+ // we make a call to the contact info cache to query for supplemental data to what the
+ // call provides. This includes the contact name and photo.
+ // This callback will always get called immediately and synchronously with whatever data
+ // it has available, and may make a subsequent call later (same thread) if it had to
+ // call into the contacts provider for more data.
+ mContactInfoCache.findInfo(call, new ContactInfoCacheCallback() {
+ @Override
+ public void onContactInfoComplete(int callId, ContactCacheEntry entry) {
+ buildAndSendNotification(state, call, entry, allowFullScreenIntent);
+ }
+ });
+
+ }
+
+ /**
+ * Sets up the main Ui for the notification
+ */
+ private void buildAndSendNotification(InCallState state, Call call,
+ ContactCacheEntry contactInfo, boolean allowFullScreenIntent) {
+
+ final int iconResId = getIconToDisplay(call);
+ final Bitmap largeIcon = getLargeIconToDisplay(contactInfo);
+ final int contentResId = getContentString(call);
+ final String contentTitle = getContentTitle(contactInfo);
+
+ // If we checked and found that nothing is different, dont issue another notification.
+ if (!checkForChangeAndSaveData(iconResId, contentResId, largeIcon, contentTitle, state,
+ allowFullScreenIntent)) {
+ return;
+ }
/*
- * Set up the Intents that will get fired when the user interacts with the notificaiton.
+ * Nothing more to check...build and send it.
*/
+ final Notification.Builder builder = getNotificationBuilder();
+
+ // Set up the main intent to send the user to the in-call screen
+ final PendingIntent inCallPendingIntent = createLaunchPendingIntent();
+ builder.setContentIntent(inCallPendingIntent);
+
+ // Set the intent as a full screen intent as well if requested
if (allowFullScreenIntent) {
configureFullScreenIntent(builder, inCallPendingIntent);
}
- /*
- * Set up notification Ui.
- */
- setUpNotification(builder, state);
+ // set the content
+ builder.setContentText(mContext.getString(contentResId));
+ builder.setSmallIcon(iconResId);
+ builder.setContentTitle(contentTitle);
+ builder.setLargeIcon(largeIcon);
+
+ // Add special Content for calls that are ongoing
+ if (InCallState.INCALL == state || InCallState.OUTGOING == state) {
+ addHangupAction(builder);
+ }
/*
* Fire off the notification
@@ -167,24 +233,129 @@
Notification notification = builder.build();
Logger.d(this, "Notifying IN_CALL_NOTIFICATION: " + notification);
mNotificationManager.notify(IN_CALL_NOTIFICATION, notification);
+ mIsShowingNotification = true;
}
/**
- * Sets up the main Ui for the notification
+ * Checks the new notification data and compares it against any notification that we
+ * are already displaying. If the data is exactly the same, we return false so that
+ * we do not issue a new notification for the exact same data.
*/
- private void setUpNotification(Notification.Builder builder, InCallState state) {
+ private boolean checkForChangeAndSaveData(int icon, int content, Bitmap largeIcon,
+ String contentTitle, InCallState state, boolean showFullScreenIntent) {
- // Add special Content for calls that are ongoing
- if (InCallState.INCALL == state || InCallState.OUTGOING == state) {
- addActiveCallIntents(builder);
+ // The two are different:
+ // if new title is not null, it should be different from saved version OR
+ // if new title is null, the saved version should not be null
+ final boolean contentTitleChanged =
+ (contentTitle != null && !contentTitle.equals(mSavedContentTitle)) ||
+ (contentTitle == null && mSavedContentTitle != null);
+
+ // any change means we are definitely updating
+ boolean retval = (mSavedIcon != icon) || (mSavedContent != content) ||
+ (mInCallState != state) || (mSavedLargeIcon != largeIcon) ||
+ contentTitleChanged;
+
+ // A full screen intent means that we have been asked to interrupt an activity,
+ // so we definitely want to show it.
+ if (showFullScreenIntent) {
+ Logger.d(this, "Forcing full screen intent");
+ retval = true;
}
- // set the content
- builder.setContentText(mContext.getString(R.string.notification_ongoing_call));
- builder.setSmallIcon(R.drawable.stat_sys_phone_call);
+ // If we aren't showing a notification right now, definitely start showing one.
+ if (!mIsShowingNotification) {
+ Logger.d(this, "Showing notification for first time.");
+ retval = true;
+ }
+
+ mSavedIcon = icon;
+ mSavedContent = content;
+ mInCallState = state;
+ mSavedLargeIcon = largeIcon;
+ mSavedContentTitle = contentTitle;
+
+ if (retval) {
+ Logger.d(this, "Data changed. Showing notification");
+ }
+
+ return retval;
}
- private void addActiveCallIntents(Notification.Builder builder) {
+ /**
+ * Returns the main string to use in the notification.
+ */
+ private String getContentTitle(ContactCacheEntry contactInfo) {
+ if (TextUtils.isEmpty(contactInfo.name)) {
+ return contactInfo.number;
+ }
+
+ return contactInfo.name;
+ }
+
+ /**
+ * Gets a large icon from the contact info object to display in the notification.
+ */
+ private Bitmap getLargeIconToDisplay(ContactCacheEntry contactInfo) {
+ if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) {
+ return ((BitmapDrawable) contactInfo.photo).getBitmap();
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the appropriate icon res Id to display based on the call for which
+ * we want to display information.
+ */
+ private int getIconToDisplay(Call call) {
+ // Even if both lines are in use, we only show a single item in
+ // the expanded Notifications UI. It's labeled "Ongoing call"
+ // (or "On hold" if there's only one call, and it's on hold.)
+ // Also, we don't have room to display caller-id info from two
+ // different calls. So if both lines are in use, display info
+ // from the foreground call. And if there's a ringing call,
+ // display that regardless of the state of the other calls.
+ if (call.getState() == Call.State.ONHOLD) {
+ return R.drawable.stat_sys_phone_call_on_hold;
+ }
+ return R.drawable.stat_sys_phone_call;
+ }
+
+ /**
+ * Returns the message to use with the notificaiton.
+ */
+ private int getContentString(Call call) {
+ int resId = R.string.notification_ongoing_call;
+
+ if (call.getState() == Call.State.INCOMING) {
+ resId = R.string.notification_incoming_call;
+
+ } else if (call.getState() == Call.State.ONHOLD) {
+ resId = R.string.notification_on_hold;
+
+ } else if (call.getState() == Call.State.DIALING) {
+ resId = R.string.notification_dialing;
+ }
+
+ return resId;
+ }
+
+ /**
+ * Gets the most relevant call to display in the notification.
+ */
+ private Call getCallToShow(CallList callList) {
+ Call call = callList.getIncomingCall();
+ if (call == null) {
+ call = callList.getOutgoingCall();
+ }
+ if (call == null) {
+ call = callList.getActiveOrBackgroundCall();
+ }
+ return call;
+ }
+
+ private void addHangupAction(Notification.Builder builder) {
Logger.i(this, "Will show \"hang-up\" action in the ongoing active call Notification");
// TODO: use better asset.
@@ -246,6 +417,53 @@
return builder;
}
+ /**
+ * Returns true if notification should not be shown in the current state.
+ */
+ private boolean shouldSuppressNotification(InCallState state, CallList callList) {
+ // Suppress the in-call notification if the InCallScreen is the
+ // foreground activity, since it's already obvious that you're on a
+ // call. (The status bar icon is needed only if you navigate *away*
+ // from the in-call UI.)
+ boolean shouldSuppress = InCallPresenter.getInstance().isShowingInCallUi();
+
+ // Suppress if the call is not active.
+ if (!state.isConnectingOrConnected()) {
+ shouldSuppress = true;
+ }
+
+ // We can still be in the INCALL state when a call is disconnected (in order to show
+ // the "Call ended" screen. So check that we have an active connection too.
+ final Call call = getCallToShow(callList);
+ if (call == null) {
+ shouldSuppress = true;
+ }
+
+ // If there's an incoming ringing call: always show the
+ // notification, since the in-call notification is what actually
+ // launches the incoming call UI in the first place (see
+ // notification.fullScreenIntent below.) This makes sure that we'll
+ // correctly handle the case where a new incoming call comes in but
+ // the InCallScreen is already in the foreground.
+ if (state.isIncoming()) {
+ shouldSuppress = false;
+ }
+
+ // JANK fix:
+ // This class will issue a notification when user makes an outgoing call.
+ // However, since we suppress the notification when the user is in the in-call screen,
+ // that results is us showing it for a split second, until the in-call screen comes up.
+ // It looks ugly.
+ //
+ // The solution is to ignore the change from HIDDEN to OUTGOING since in that particular
+ // case, we know we'll get called to update again when the UI finally starts.
+ if (InCallState.OUTGOING == state && InCallState.HIDDEN == mInCallState) {
+ shouldSuppress = true;
+ }
+
+ return shouldSuppress;
+ }
+
private PendingIntent createLaunchPendingIntent() {
final Intent intent = new Intent(Intent.ACTION_MAIN, null);