From 42373eb59cbef15ec61ebb5c919031f293291a53 Mon Sep 17 00:00:00 2001 From: Chiao Cheng Date: Thu, 22 Aug 2013 18:36:24 -0700 Subject: Major fixes for in call to work with reverse number lookup. - Separated caller info data into CallIdentification and switch callbacks to use it where call state is un-necessary. - Changed mCallList.update() method to be onIncoming(). - Catch all exceptions from service methods so errors do not vanish. - Fixed bind failure cases which led to DeadObjectException. - Changed local contact lookup to only occur for incoming calls. - Fixed CallCardPresenter to start contact search upon isntantiation instead of waiting for next call update. - Convert ContactInfoCache to singleton to avoid race condition where it's not initialized. - Handle cases where primary call may be null when we find a contact. - Fixed race conditions in CallButtonPresenter where audio mode is being set before ui is ready. - Fixed race condition in AnswerPresenter where state change was being called before ui is ready. - Changes to CallCardPresenter to support lookup for conference calls. Bug: 10413515 Bug: 10390984 Change-Id: I9fc4f2f35e8f5aad33c301b3c5c93132634cb63c --- InCallUI/res/layout/answer_fragment.xml | 1 + .../src/com/android/incallui/AnswerFragment.java | 31 +- .../src/com/android/incallui/AnswerPresenter.java | 87 ++++-- .../src/com/android/incallui/BaseFragment.java | 4 +- .../com/android/incallui/CallButtonPresenter.java | 9 +- .../src/com/android/incallui/CallCardFragment.java | 86 +++--- .../com/android/incallui/CallCardPresenter.java | 210 ++++++++----- .../com/android/incallui/CallHandlerService.java | 83 ++++-- InCallUI/src/com/android/incallui/CallList.java | 90 +++++- .../src/com/android/incallui/CallerInfoUtils.java | 43 ++- .../src/com/android/incallui/ContactInfoCache.java | 324 +++++++++++---------- .../src/com/android/incallui/InCallActivity.java | 8 +- .../src/com/android/incallui/InCallPresenter.java | 29 +- .../com/android/incallui/StatusBarNotifier.java | 53 ++-- 14 files changed, 651 insertions(+), 407 deletions(-) diff --git a/InCallUI/res/layout/answer_fragment.xml b/InCallUI/res/layout/answer_fragment.xml index f6b132091..5f8c561d3 100644 --- a/InCallUI/res/layout/answer_fragment.xml +++ b/InCallUI/res/layout/answer_fragment.xml @@ -30,6 +30,7 @@ android:gravity="center" android:layout_gravity="bottom|center_horizontal" android:background="@android:color/black" + android:visibility="gone" dc:targetDrawables="@array/incoming_call_widget_2way_targets" dc:targetDescriptions="@array/incoming_call_widget_2way_target_descriptions" diff --git a/InCallUI/src/com/android/incallui/AnswerFragment.java b/InCallUI/src/com/android/incallui/AnswerFragment.java index 077eaac42..40462ce1f 100644 --- a/InCallUI/src/com/android/incallui/AnswerFragment.java +++ b/InCallUI/src/com/android/incallui/AnswerFragment.java @@ -16,8 +16,6 @@ package com.android.incallui; -import com.google.common.base.Preconditions; - import android.app.AlertDialog; import android.app.Dialog; import android.os.Bundle; @@ -28,6 +26,8 @@ import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.ListView; +import com.google.common.base.Preconditions; + import java.util.ArrayList; /** @@ -39,10 +39,9 @@ public class AnswerFragment extends BaseFragment textResponses) { textResponses.add(getResources().getString(R.string.respond_via_sms_custom_message)); mTextResponsesAdapter = new ArrayAdapter(getActivity(), - android.R.layout.simple_list_item_1, - android.R.id.text1, - textResponses); + android.R.layout.simple_list_item_1, android.R.id.text1, textResponses); } @Override @@ -151,14 +146,14 @@ public class AnswerFragment extends BaseFragment parent, // The ListView - View view, // The TextView that was clicked - int position, - long id) { + View view, // The TextView that was clicked + int position, long id) { Log.d(this, "RespondViaSmsItemClickListener.onItemClick(" + position + ")..."); final String message = (String) parent.getItemAtPosition(position); Log.v(this, "- message: '" + message + "'"); diff --git a/InCallUI/src/com/android/incallui/AnswerPresenter.java b/InCallUI/src/com/android/incallui/AnswerPresenter.java index 78ee0f1a3..1c970d7db 100644 --- a/InCallUI/src/com/android/incallui/AnswerPresenter.java +++ b/InCallUI/src/com/android/incallui/AnswerPresenter.java @@ -16,8 +16,6 @@ package com.android.incallui; -import com.android.incallui.InCallPresenter.InCallState; -import com.android.incallui.InCallPresenter.InCallStateListener; import com.android.services.telephony.common.Call; import java.util.ArrayList; @@ -26,53 +24,94 @@ import java.util.ArrayList; * Presenter for the Incoming call widget. */ public class AnswerPresenter extends Presenter - implements InCallStateListener { + implements CallList.CallUpdateListener, CallList.Listener { - private Call mCall; - private ArrayList mTextResponses; + private static final String TAG = AnswerPresenter.class.getSimpleName(); + + private int mCallId = Call.INVALID_CALL_ID; @Override public void onUiReady(AnswerUi ui) { super.onUiReady(ui); + + final CallList calls = CallList.getInstance(); + final Call call = calls.getIncomingCall(); + // TODO: change so that answer presenter never starts up if it's not incoming. + if (call != null) { + processIncomingCall(call); + + // Listen for call updates for the current call. + calls.addCallUpdateListener(mCallId, this); + + // Listen for incoming calls. + calls.addListener(this); + } } @Override - public void onStateChange(InCallState state, CallList callList) { - if (state == InCallState.INCOMING) { - getUi().showAnswerUi(true); - mCall = callList.getIncomingCall(); - mTextResponses = callList.getTextResponses(mCall); - if (mTextResponses != null) { - getUi().showTextButton(true); - getUi().configureMessageDialogue(mTextResponses); - } else { - getUi().showTextButton(false); + public void onCallListChange(CallList callList) { + // no-op + } + + @Override + public void onIncomingCall(Call call) { + // TODO: Ui is being destroyed when the fragment detaches. Need clean up step to stop + // getting updates here. + if (getUi() != null) { + if (call.getCallId() != mCallId) { + // A new call is coming in. + processIncomingCall(call); } - Log.d(this, "Showing incoming with: " + mCall); + } + } + + private void processIncomingCall(Call call) { + mCallId = call.getCallId(); + Log.d(TAG, "Showing incoming for call id: " + mCallId); + final ArrayList textMsgs = CallList.getInstance().getTextResponses( + call.getCallId()); + getUi().showAnswerUi(true); + + if (textMsgs != null) { + getUi().showTextButton(true); + getUi().configureMessageDialogue(textMsgs); } else { + getUi().showTextButton(false); + } + } + + + @Override + public void onCallStateChanged(Call call) { + Log.d(TAG, "onCallStateChange() " + call); + if (call.getState() != Call.State.INCOMING) { + // Stop listening for updates. + CallList.getInstance().removeCallUpdateListener(mCallId, this); + CallList.getInstance().removeListener(this); + getUi().showAnswerUi(false); - mCall = null; + mCallId = Call.INVALID_CALL_ID; } } public void onAnswer() { - if (mCall == null) { + if (mCallId == Call.INVALID_CALL_ID) { return; } - Log.d(this, "onAnswer " + mCall.getCallId()); + Log.d(this, "onAnswer " + mCallId); - CallCommandClient.getInstance().answerCall(mCall.getCallId()); + CallCommandClient.getInstance().answerCall(mCallId); } public void onDecline() { - if (mCall == null) { + if (mCallId == Call.INVALID_CALL_ID) { return; } - Log.d(this, "onDecline " + mCall.getCallId()); + Log.d(this, "onDecline " + mCallId); - CallCommandClient.getInstance().rejectCall(mCall.getCallId(), false, null); + CallCommandClient.getInstance().rejectCall(mCallId, false, null); } public void onText() { @@ -82,7 +121,7 @@ public class AnswerPresenter extends Presenter public void rejectCallWithMessage(String message) { Log.d(this, "sendTextToDefaultActivity()..."); - CallCommandClient.getInstance().rejectCall(mCall.getCallId(), true, message); + CallCommandClient.getInstance().rejectCall(mCallId, true, message); getUi().dismissPopup(); } diff --git a/InCallUI/src/com/android/incallui/BaseFragment.java b/InCallUI/src/com/android/incallui/BaseFragment.java index 0f3d6b4ae..a348ce49a 100644 --- a/InCallUI/src/com/android/incallui/BaseFragment.java +++ b/InCallUI/src/com/android/incallui/BaseFragment.java @@ -47,8 +47,8 @@ public abstract class BaseFragment, U extends Ui> extends } @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); mPresenter.onUiReady(getUi()); } } diff --git a/InCallUI/src/com/android/incallui/CallButtonPresenter.java b/InCallUI/src/com/android/incallui/CallButtonPresenter.java index 3a2d719e9..9db383fdd 100644 --- a/InCallUI/src/com/android/incallui/CallButtonPresenter.java +++ b/InCallUI/src/com/android/incallui/CallButtonPresenter.java @@ -39,6 +39,7 @@ public class CallButtonPresenter extends Presenter - implements InCallStateListener, AudioModeListener, ContactInfoCacheCallback { + implements InCallStateListener, AudioModeListener { private static final String TAG = CallCardPresenter.class.getSimpleName(); private static final long CALL_TIME_UPDATE_INTERVAL = 1000; // in milliseconds private PhoneNumberService mPhoneNumberService; private AudioModeProvider mAudioModeProvider; - private ContactInfoCache mContactInfoCache; private Call mPrimary; private Call mSecondary; private ContactCacheEntry mPrimaryContactInfo; @@ -65,15 +66,33 @@ public class CallCardPresenter extends Presenter }); } - public void init(Context context, PhoneNumberService phoneNumberService) { + + public void init(Context context, PhoneNumberService phoneNumberService, Call call) { + mContext = Preconditions.checkNotNull(context); + mPhoneNumberService = Preconditions.checkNotNull(phoneNumberService); + Preconditions.checkNotNull(call); mContext = context; mPhoneNumberService = phoneNumberService; + final CallIdentification identification = call.getIdentification(); + + // TODO(klp): Logic to determine which ui field get what data resides in contactInfoCache. + // It needs to be moved so it can be re-used. + mPrimaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext, identification, + call.getState() == Call.State.INCOMING); + + // start processing lookups right away. + startContactInfoSearch(identification, true, false, call.getState() == Call.State.INCOMING); } @Override public void onUiReady(CallCardUi ui) { super.onUiReady(ui); + // Contact search may have completed before ui is ready. + if (mPrimaryContactInfo != null) { + updatePrimaryDisplayInfo(mPrimaryContactInfo, false); + } + if (mAudioModeProvider != null) { mAudioModeProvider.addListener(this); } @@ -93,7 +112,7 @@ public class CallCardPresenter extends Presenter @Override public void onStateChange(InCallState state, CallList callList) { - Log.d(TAG, "onStateChange()"); + Log.d(TAG, "onStateChange() " + state); final CallCardUi ui = getUi(); if (ui == null) { return; @@ -118,15 +137,35 @@ public class CallCardPresenter extends Presenter Log.d(this, "Primary call: " + primary); Log.d(this, "Secondary call: " + secondary); + if (primary != null) { + if (mPrimary == null || mPrimary.getCallId() != primary.getCallId()) { + // primary call has changed + mPrimaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext, + primary.getIdentification(), primary.getState() == Call.State.INCOMING); + updatePrimaryDisplayInfo(mPrimaryContactInfo, isConference(primary)); + startContactInfoSearch(primary.getIdentification(), true, + primary.isConferenceCall(), primary.getState() == Call.State.INCOMING); + } + } + + if (secondary == null) { + // Secondary call may have ended. Update the ui. + mSecondaryContactInfo = null; + updateSecondaryDisplayInfo(); + } else { + if (mSecondary == null || mSecondary.getCallId() != secondary.getCallId()) { + // secondary call has changed + mSecondaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext, + secondary.getIdentification(), secondary.getState() == Call.State.INCOMING); + updateSecondaryDisplayInfo(); + startContactInfoSearch(secondary.getIdentification(), false, + secondary.isConferenceCall(), secondary.getState() == Call.State.INCOMING); + } + } + mPrimary = primary; mSecondary = secondary; - // 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(); - // Start/Stop the call time update timer if (mPrimary != null && mPrimary.getState() == Call.State.ACTIVE) { Log.d(this, "Starting the calltime timer"); @@ -175,27 +214,62 @@ public class CallCardPresenter extends Presenter } } - - public void setContactInfoCache(ContactInfoCache cache) { - mContactInfoCache = cache; - startContactInfoSearch(); - } - /** * 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); + private void startContactInfoSearch(final CallIdentification identification, + final boolean isPrimary, final boolean isConference, boolean isIncoming) { + + final ContactInfoCache cache = ContactInfoCache.getInstance(mContext); + + // See if local contact lookup was already made by the status bar (for incoming calls) + final ContactCacheEntry entry = cache.getInfo(identification.getCallId()); + + // TODO: un-stable... must use number field to check if a contact was found + // because the contactinfocache pre-massages the data into the ui fields. + // Need to do massaging outside of contactinfocache. + if (entry == null || entry.number == null) { + // TODO(klp): currently we can't distinguish between... + // 1) a lookup occurred but failed to find a local contact. + // 2) a lookup has not occurred. + // We need to track it so we can avoid an un-necessary lookup here. + Log.d(TAG, "Local contact cache does not contain the contact. Searching provider."); + cache.findInfo(identification, isIncoming, new ContactInfoCacheCallback() { + @Override + public void onContactInfoComplete(int callId, ContactCacheEntry entry) { + // TODO: un-stable... must use label field to check if a contact was found + // because the contactinfocache pre-massages the data into the ui fields. + // Need to do massaging outside of contactinfocache. + if (entry.label == null) { + // Name not found. Try lookup. + Log.d(TAG, "Local contact not found, performing reverse lookup."); + lookupPhoneNumber(identification.getNumber()); + } else { + Log.d(TAG, "Found contact in provider: " + entry); + updateContactEntry(entry, isPrimary, isConference); + } + } + }); } else { - mPrimaryContactInfo = null; - updatePrimaryDisplayInfo(); + Log.d(TAG, "Found contact in cache: " + entry); + updateContactEntry(entry, isPrimary, isConference); + } + } + + private boolean isConference(Call call) { + if (call == null) { + return false; } + return call.isConferenceCall(); + } - if (mSecondary != null && mContactInfoCache != null) { - mContactInfoCache.findInfo(mSecondary, this); + private void updateContactEntry(ContactCacheEntry entry, boolean isPrimary, + boolean isConference) { + if (isPrimary) { + mPrimaryContactInfo = entry; + updatePrimaryDisplayInfo(entry, isConference); } else { - mSecondaryContactInfo = null; + mSecondaryContactInfo = entry; updateSecondaryDisplayInfo(); } } @@ -238,38 +312,25 @@ public class CallCardPresenter extends Presenter return retval; } - /** - * Callback received when Contact info data query completes. - */ - @Override - public void onContactInfoComplete(int callId, ContactCacheEntry entry) { - if (mPrimary != null && mPrimary.getCallId() == callId) { - mPrimaryContactInfo = entry; - updatePrimaryDisplayInfo(); - lookupPhoneNumber(mPrimary.getNumber()); - } - if (mSecondary != null && mSecondary.getCallId() == callId) { - mSecondaryContactInfo = entry; - updateSecondaryDisplayInfo(); - // TODO(klp): investigate reverse lookup for secondary call. - } - - } - - private void updatePrimaryDisplayInfo() { + private void updatePrimaryDisplayInfo(ContactCacheEntry entry, boolean isConference) { + Log.d(TAG, "Update primary display " + entry); final CallCardUi ui = getUi(); if (ui == null) { + // TODO: May also occur if search result comes back after ui is destroyed. Look into + // removing that case completely. + Log.d(TAG, "updatePrimaryDisplayInfo called but ui is null!"); return; } - if (mPrimaryContactInfo != null) { - final String name = getNameForCall(mPrimaryContactInfo); - final String number = getNumberForCall(mPrimaryContactInfo); - final boolean nameIsNumber = name != null && name.equals(mPrimaryContactInfo.number); + if (entry != null) { + final String name = getNameForCall(entry); + final String number = getNumberForCall(entry); + final boolean nameIsNumber = name != null && name.equals(entry.number); final String gatewayLabel = getGatewayLabel(); final String gatewayNumber = getGatewayNumber(); - ui.setPrimary(number, name, nameIsNumber, mPrimaryContactInfo.label, - mPrimaryContactInfo.photo, mPrimary.isConferenceCall(), gatewayLabel, + + ui.setPrimary(number, name, nameIsNumber, entry.label, + entry.photo, isConference, gatewayLabel, gatewayNumber); } else { // reset to nothing (like at end of call) @@ -278,6 +339,23 @@ public class CallCardPresenter extends Presenter } + private void updateSecondaryDisplayInfo() { + + final CallCardUi ui = getUi(); + if (ui == null) { + return; + } + + if (mSecondaryContactInfo != null) { + Log.d(TAG, "updateSecondaryDisplayInfo() " + mSecondaryContactInfo); + ui.setSecondary(true, getNameForCall(mSecondaryContactInfo), + 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); + } + } + public void lookupPhoneNumber(String phoneNumber) { if (mPhoneNumberService != null) { mPhoneNumberService.getPhoneNumberInfo(phoneNumber, @@ -291,7 +369,7 @@ public class CallCardPresenter extends Presenter // TODO(klp): Ui is sometimes null due to something being shutdown. if (getUi() != null) { if (info.getName() != null) { - getUi().setName(info.getName()); + getUi().setPrimaryName(info.getName(), false); } if (info.getImageUrl() != null) { @@ -333,6 +411,12 @@ public class CallCardPresenter extends Presenter private boolean hasOutgoingGatewayCall() { // We only display the gateway information while DIALING so return false for any othe // call state. + // TODO: mPrimary can be null because this is called from updatePrimaryDisplayInfo which + // is also called after a contact search completes (call is not present yet). Split the + // UI update so it can receive independent updates. + if (mPrimary == null) { + return false; + } return (mPrimary.getState() == Call.State.DIALING && !TextUtils.isEmpty(mPrimary.getGatewayNumber()) && !TextUtils.isEmpty(mPrimary.getGatewayPackage())); @@ -352,7 +436,7 @@ public class CallCardPresenter extends Presenter protected void onPostExecute(Bitmap bitmap) { // TODO(klp): same as above, figure out why it's null. if (getUi() != null) { - getUi().setImage(bitmap); + getUi().setPrimaryImage(bitmap); } } @@ -382,22 +466,6 @@ public class CallCardPresenter extends Presenter return contactInfo.number; } - private void updateSecondaryDisplayInfo() { - final CallCardUi ui = getUi(); - if (ui == null) { - return; - } - - if (mSecondaryContactInfo != null) { - final String name = getNameForCall(mSecondaryContactInfo); - ui.setSecondary(true, getNameForCall(mSecondaryContactInfo), - 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); - } - } - public void setAudioModeProvider(AudioModeProvider audioModeProvider) { mAudioModeProvider = audioModeProvider; mAudioModeProvider.addListener(this); @@ -409,8 +477,12 @@ public class CallCardPresenter extends Presenter Drawable photo, boolean isConference, String gatewayLabel, String gatewayNumber); void setSecondary(boolean show, String name, String label, Drawable photo); void setCallState(int state, Call.DisconnectCause cause, boolean bluetoothOn); + void setPrimaryCallElapsedTime(boolean show, String duration); - void setName(String name); - void setImage(Bitmap bitmap); + void setPrimaryName(String name, boolean nameIsNumber); + void setPrimaryImage(Bitmap bitmap); + void setPrimaryPhoneNumber(String phoneNumber); + void setPrimaryLabel(String label); + void setPrimaryGateway(String label, String number); } } diff --git a/InCallUI/src/com/android/incallui/CallHandlerService.java b/InCallUI/src/com/android/incallui/CallHandlerService.java index 150935075..76237e106 100644 --- a/InCallUI/src/com/android/incallui/CallHandlerService.java +++ b/InCallUI/src/com/android/incallui/CallHandlerService.java @@ -36,6 +36,8 @@ import java.util.Map; */ public class CallHandlerService extends Service { + private final static String TAG = CallHandlerService.class.getSimpleName(); + private static final int ON_UPDATE_CALL = 1; private static final int ON_UPDATE_MULTI_CALL = 2; private static final int ON_UPDATE_CALL_WITH_TEXT_RESPONSES = 3; @@ -56,8 +58,8 @@ public class CallHandlerService extends Service { Log.d(this, "onCreate started"); super.onCreate(); - mCallList = new CallList(); mMainHandler = new MainHandler(); + mCallList = CallList.getInstance(); mAudioModeProvider = new AudioModeProvider(); mInCallPresenter = InCallPresenter.getInstance(); mInCallPresenter.setUp(getApplicationContext(), mCallList, mAudioModeProvider); @@ -105,54 +107,73 @@ public class CallHandlerService extends Service { @Override public void setCallCommandService(ICallCommandService service) { - Log.d(CallHandlerService.this, "onConnected: " + service.toString()); - CallCommandClient.getInstance().setService(service); + try { + Log.d(CallHandlerService.this, "onConnected: " + service.toString()); + CallCommandClient.getInstance().setService(service); + } catch (Exception e) { + Log.e(TAG, "Error processing setCallCommandservice() call", e); + } } @Override public void onDisconnect(Call call) { - Log.d(CallHandlerService.this, "onDisconnected: " + call); - mMainHandler.sendMessage(mMainHandler.obtainMessage(ON_DISCONNECT_CALL, 0, 0, call)); + try { + Log.d(CallHandlerService.this, "onDisconnected: " + call); + mMainHandler.sendMessage(mMainHandler.obtainMessage(ON_DISCONNECT_CALL, call)); + } catch (Exception e) { + Log.e(TAG, "Error processing onDisconnect() call.", e); + } } @Override public void onIncoming(Call call, List textResponses) { - Log.d(CallHandlerService.this, "onIncomingCall: " + call); - - // TODO(klp): Add text responses to the call object. - Map.Entry > incomingCall = new AbstractMap.SimpleEntry >(call, textResponses); - mMainHandler.sendMessage(mMainHandler.obtainMessage(ON_UPDATE_CALL_WITH_TEXT_RESPONSES, - 0, 0, incomingCall)); + try { + Log.d(CallHandlerService.this, "onIncomingCall: " + call); + + // TODO(klp): Add text responses to the call object. + Map.Entry> incomingCall + = new AbstractMap.SimpleEntry>(call, textResponses); + Log.d("TEST", mMainHandler.toString()); + mMainHandler.sendMessage(mMainHandler.obtainMessage( + ON_UPDATE_CALL_WITH_TEXT_RESPONSES, incomingCall)); + } catch (Exception e) { + Log.e(TAG, "Error processing onIncoming() call.", e); + } } @Override - public void onUpdate(List calls, boolean fullUpdate) { - Log.d(CallHandlerService.this, "onUpdate " + calls.toString()); - - if (Log.VERBOSE) { - for (Call c : calls) { - Log.v(this, "Call: " + c); - } + public void onUpdate(List calls) { + try { + Log.d(CallHandlerService.this, "onUpdate " + calls.toString()); + + // TODO(klp): Add use of fullUpdate to message + mMainHandler.sendMessage(mMainHandler.obtainMessage(ON_UPDATE_MULTI_CALL, calls)); + } catch (Exception e) { + Log.e(TAG, "Error processing onUpdate() call.", e); } - - // TODO(klp): Add use of fullUpdate to message - mMainHandler.sendMessage(mMainHandler.obtainMessage(ON_UPDATE_MULTI_CALL, 0, 0, calls)); } @Override public void onAudioModeChange(int mode) { - Log.d(CallHandlerService.this, "onAudioModeChange : " + AudioMode.toString(mode)); - mMainHandler.sendMessage(mMainHandler.obtainMessage(ON_AUDIO_MODE, mode, 0, null)); + try { + Log.d(CallHandlerService.this, "onAudioModeChange : " + AudioMode.toString(mode)); + mMainHandler.sendMessage(mMainHandler.obtainMessage(ON_AUDIO_MODE, mode, 0, null)); + } catch (Exception e) { + Log.e(TAG, "Error processing onAudioModeChange() call.", e); + } } @Override public void onSupportedAudioModeChange(int modeMask) { - Log.d(CallHandlerService.this, "onSupportedAudioModeChange : " + - AudioMode.toString(modeMask)); - - mMainHandler.sendMessage( - mMainHandler.obtainMessage(ON_SUPPORTED_AUDIO_MODE, modeMask, 0, null)); + try { + Log.d(CallHandlerService.this, "onSupportedAudioModeChange : " + AudioMode.toString( + modeMask)); + + mMainHandler.sendMessage(mMainHandler.obtainMessage(ON_SUPPORTED_AUDIO_MODE, + modeMask, 0, null)); + } catch (Exception e) { + Log.e(TAG, "Error processing onSupportedAudioModeChange() call.", e); + } } }; @@ -188,7 +209,9 @@ public class CallHandlerService extends Service { mCallList.onUpdate((List) msg.obj); break; case ON_UPDATE_CALL_WITH_TEXT_RESPONSES: - mCallList.onUpdate((AbstractMap.SimpleEntry >) msg.obj); + AbstractMap.SimpleEntry> entry + = (AbstractMap.SimpleEntry>) msg.obj; + mCallList.onIncoming(entry.getKey(), entry.getValue()); break; case ON_DISCONNECT_CALL: mCallList.onDisconnect((Call) msg.obj); diff --git a/InCallUI/src/com/android/incallui/CallList.java b/InCallUI/src/com/android/incallui/CallList.java index 7890b997a..4ce04d35a 100644 --- a/InCallUI/src/com/android/incallui/CallList.java +++ b/InCallUI/src/com/android/incallui/CallList.java @@ -16,6 +16,7 @@ package com.android.incallui; +import com.google.android.collect.Lists; import com.google.android.collect.Maps; import com.google.android.collect.Sets; import com.google.common.base.Preconditions; @@ -25,7 +26,6 @@ import android.os.Message; import com.android.services.telephony.common.Call; -import java.util.AbstractMap; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -42,27 +42,27 @@ public class CallList { private static final int EVENT_DISCONNECTED_TIMEOUT = 1; - private static CallList sInstance; + private static CallList sInstance = new CallList(); private final HashMap mCallMap = Maps.newHashMap(); private final HashMap> mCallTextReponsesMap = Maps.newHashMap(); private final Set mListeners = Sets.newArraySet(); + private final HashMap> mCallUpdateListenerMap = Maps + .newHashMap(); + /** * Static singleton accessor method. */ - /*public static synchronized CallList getInstance() { - if (sInstance == null) { - sInstance = new CallList(); - } + public static CallList getInstance() { return sInstance; - }*/ + } /** * Private constructor. Instance should only be acquired through getInstance(). */ - public CallList() { + private CallList() { } /** @@ -89,13 +89,15 @@ public class CallList { /** * Called when a single call has changed. */ - public void onUpdate(AbstractMap.SimpleEntry > incomingCall) { - Log.d(this, "onUpdate - " + incomingCall.getKey()); + public void onIncoming(Call call, List textMessages) { + Log.d(this, "onIncoming - " + call); - updateCallInMap(incomingCall.getKey()); - updateCallTextMap(incomingCall.getKey(), incomingCall.getValue()); + updateCallInMap(call); + updateCallTextMap(call, textMessages); - notifyListenersOfChange(); + for (Listener listener : mListeners) { + listener.onIncomingCall(call); + } } /** @@ -110,11 +112,50 @@ public class CallList { updateCallInMap(call); updateCallTextMap(call, null); + + notifyCallUpdateListeners(call); } notifyListenersOfChange(); } + public void notifyCallUpdateListeners(Call call) { + final List listeners = mCallUpdateListenerMap.get(call.getCallId()); + if (listeners != null) { + for (CallUpdateListener listener : listeners) { + listener.onCallStateChanged(call); + } + } + } + + /** + * Add a call update listener for a call id. + * + * @param callId The call id to get updates for. + * @param listener The listener to add. + */ + public void addCallUpdateListener(int callId, CallUpdateListener listener) { + List listeners = mCallUpdateListenerMap.get(callId); + if (listeners == null) { + listeners = Lists.newArrayList(); + mCallUpdateListenerMap.put(callId, listeners); + } + listeners.add(listener); + } + + /** + * Remove a call update listener for a call id. + * + * @param callId The call id to remove the listener for. + * @param listener The listener to remove. + */ + public void removeCallUpdateListener(int callId, CallUpdateListener listener) { + List listeners = mCallUpdateListenerMap.get(callId); + if (listeners != null) { + listeners.remove(listener); + } + } + public void addListener(Listener listener) { Preconditions.checkNotNull(listener); @@ -179,6 +220,19 @@ public class CallList { return call; } + + public Call getFirstCall() { + // TODO: should we switch to a simple list and pull the first one? + Call result = getIncomingCall(); + if (result == null) { + result = getFirstCallWithState(Call.State.DIALING); + } + if (result == null) { + result = getFirstCallWithState(Call.State.ACTIVE); + } + return result; + } + public boolean existsLiveCall() { for (Call call : mCallMap.values()) { if (!isCallDead(call)) { @@ -188,8 +242,8 @@ public class CallList { return false; } - public ArrayList getTextResponses(Call call) { - return mCallTextReponsesMap.get(call.getCallId()); + public ArrayList getTextResponses(int callId) { + return mCallTextReponsesMap.get(callId); } /** @@ -327,5 +381,11 @@ public class CallList { */ public interface Listener { public void onCallListChange(CallList callList); + public void onIncomingCall(Call call); + } + + public interface CallUpdateListener { + // TODO: refactor and limit arg to be call state. Caller info is not needed. + public void onCallStateChanged(Call call); } } diff --git a/InCallUI/src/com/android/incallui/CallerInfoUtils.java b/InCallUI/src/com/android/incallui/CallerInfoUtils.java index 690b9b7b7..8d1fc9f03 100644 --- a/InCallUI/src/com/android/incallui/CallerInfoUtils.java +++ b/InCallUI/src/com/android/incallui/CallerInfoUtils.java @@ -4,6 +4,7 @@ import android.content.Context; import android.text.TextUtils; import com.android.services.telephony.common.Call; +import com.android.services.telephony.common.CallIdentification; import java.util.Arrays; @@ -28,40 +29,36 @@ public class CallerInfoUtils { * 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, + public static CallerInfo getCallerInfoForCall(Context context, CallIdentification call, CallerInfoAsyncQuery.OnQueryCompleteListener listener) { - CallerInfo info = new CallerInfo(); + CallerInfo info = buildCallerInfo(context, call); String number = call.getNumber(); + // TODO: Have phoneapp send a Uri when it knows the contact that triggered this call. + + if (info.numberPresentation == Call.PRESENTATION_ALLOWED) { + // Start the query with the number provided from the call. + Log.d(TAG, "==> Actually starting CallerInfoAsyncQuery.startQuery()..."); + CallerInfoAsyncQuery.startQuery(QUERY_TOKEN, context, number, listener, call); + } + return info; + } + + public static CallerInfo buildCallerInfo(Context context, CallIdentification identification) { + CallerInfo info = new CallerInfo(); + // Store CNAP information retrieved from the Connection (we want to do this // here regardless of whether the number is empty or not). - info.cnapName = call.getCnapName(); + info.cnapName = identification.getCnapName(); info.name = info.cnapName; - info.numberPresentation = call.getNumberPresentation(); - info.namePresentation = call.getCnapNamePresentation(); - - // TODO: Have phoneapp send a Uri when it knows the contact that triggered this call. + info.numberPresentation = identification.getNumberPresentation(); + info.namePresentation = identification.getCnapNamePresentation(); + String number = identification.getNumber(); 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. - Log.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 index e62900ea9..a80d91c82 100644 --- a/InCallUI/src/com/android/incallui/ContactInfoCache.java +++ b/InCallUI/src/com/android/incallui/ContactInfoCache.java @@ -16,10 +16,6 @@ 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; @@ -31,7 +27,14 @@ import android.provider.ContactsContract.Contacts; import android.text.TextUtils; import com.android.services.telephony.common.Call; +import com.android.services.telephony.common.CallIdentification; +import com.android.services.telephony.common.MoreStrings; +import com.google.android.collect.Lists; +import com.google.android.collect.Maps; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -42,73 +45,94 @@ import java.util.Map; * 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 { +public class ContactInfoCache implements ContactsAsyncHelper.OnImageLoadCompleteListener { + private static final String TAG = ContactInfoCache.class.getSimpleName(); private static final int TOKEN_UPDATE_PHOTO_FOR_CALL_STATE = 0; private final Context mContext; - private final Map mInfoMap = Maps.newHashMap(); + private final HashMap mInfoMap = Maps.newHashMap(); + private final HashMap> mCallBacks = Maps.newHashMap(); - public ContactInfoCache(Context context) { + private static ContactInfoCache sCache = null; + + public static synchronized ContactInfoCache getInstance(Context mContext) { + if (sCache == null) { + sCache = new ContactInfoCache(mContext); + } + return sCache; + } + + private ContactInfoCache(Context context) { mContext = context; } + public ContactCacheEntry getInfo(int callId) { + return mInfoMap.get(callId); + } + + public static ContactCacheEntry buildCacheEntryFromCall(Context context, + CallIdentification identification, boolean isIncoming) { + final ContactCacheEntry entry = new ContactCacheEntry(); + + // TODO: get rid of caller info. + final CallerInfo info = CallerInfoUtils.buildCallerInfo(context, identification); + ContactInfoCache.populateCacheEntry(context, info, entry, + identification.getNumberPresentation(), isIncoming); + return entry; + } + /** * 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 identification The call identification * @param callback The function to call back when the call is found. Can be null. */ - public void findInfo(Call call, ContactInfoCacheCallback callback) { + public void findInfo(final CallIdentification identification, final boolean isIncoming, + ContactInfoCacheCallback callback) { Preconditions.checkState(Looper.getMainLooper().getThread() == Thread.currentThread()); Preconditions.checkNotNull(callback); - Preconditions.checkNotNull(call); - - final SearchEntry entry; + final int callId = identification.getCallId(); // 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); - } + List callBacks = mCallBacks.get(callId); + if (callBacks == null) { + + // New lookup + callBacks = Lists.newArrayList(); + callBacks.add(callback); + mCallBacks.put(callId, callBacks); + + /** + * 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. + */ + CallerInfoUtils.getCallerInfoForCall(mContext, identification, + new CallerInfoAsyncQuery.OnQueryCompleteListener() { + @Override + public void onQueryComplete(int token, Object cookie, CallerInfo ci) { + int presentationMode = identification.getNumberPresentation(); + if (ci.contactExists || ci.isEmergencyNumber() || ci + .isVoiceMailNumber()) { + presentationMode = Call.PRESENTATION_ALLOWED; + } + + // This starts the photo load. + final ContactCacheEntry cacheEntry = buildEntry(mContext, + identification.getCallId(), ci, presentationMode, isIncoming, + ContactInfoCache.this); + + // Add the contact info to the cache. + mInfoMap.put(callId, cacheEntry); + sendNotification(identification.getCallId(), cacheEntry); + } + }); } 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); + callBacks.add(callback); } } @@ -124,30 +148,30 @@ public class ContactInfoCache implements CallerInfoAsyncQuery.OnQueryCompleteLis // TODO (klp): What is this, and why does it need the write_contacts permission? // CallerInfoUtils.sendViewNotificationAsync(mContext, mLoadingPersonUri); - final Call call = (Call) cookie; + final int callId = (Integer) cookie; - if (!mInfoMap.containsKey(call.getCallId())) { + if (!mInfoMap.containsKey(callId)) { Log.e(this, "Image Load received for empty search entry."); return; } - final SearchEntry entry = mInfoMap.get(call.getCallId()); + final ContactCacheEntry entry = mInfoMap.get(callId); Log.d(this, "setting photo for entry: ", entry); // TODO (klp): Handle conference calls if (photo != null) { Log.v(this, "direct drawable: ", photo); - entry.info.photo = photo; + entry.photo = photo; } else if (photoIcon != null) { Log.v(this, "photo icon: ", photoIcon); - entry.info.photo = new BitmapDrawable(mContext.getResources(), photoIcon); + entry.photo = new BitmapDrawable(mContext.getResources(), photoIcon); } else { Log.v(this, "unknown photo"); - entry.info.photo = null; + entry.photo = null; } - sendNotification(entry); + sendNotification(callId, entry); } /** @@ -155,33 +179,63 @@ public class ContactInfoCache implements CallerInfoAsyncQuery.OnQueryCompleteLis */ public void clearCache() { mInfoMap.clear(); + mCallBacks.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); + private static ContactCacheEntry buildEntry(Context context, int callId, + CallerInfo info, int presentation, boolean isIncoming, + ContactsAsyncHelper.OnImageLoadCompleteListener imageLoadListener) { + // The actual strings we're going to display onscreen: + Drawable photo = null; + + final ContactCacheEntry cce = new ContactCacheEntry(); + populateCacheEntry(context, info, cce, presentation, isIncoming); + + // This will only be true for emergency numbers + if (info.photoResource != 0) { + photo = context.getResources().getDrawable(info.photoResource); + } else if (info.isCachedPhotoCurrent) { + if (info.cachedPhoto != null) { + photo = info.cachedPhoto; + } else { + photo = context.getResources().getDrawable(R.drawable.picture_unknown); + } + } else { + Uri personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, info.person_id); + Log.d(TAG, "- got personUri: '" + personUri + "', based on info.person_id: " + + info.person_id); - updateCallerInfo(entry, ci, entry.call.getNumberPresentation()); + if (personUri == null) { + Log.v(TAG, "personUri is null. Just use unknown picture."); + photo = context.getResources().getDrawable(R.drawable.picture_unknown); + } else { + Log.d(TAG, "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, + context, personUri, imageLoadListener, callId); + + // 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); + } + } + + cce.photo = photo; + return cce; } - private void updateCallerInfo(SearchEntry entry, CallerInfo info, int presentation) { - // The actual strings we're going to display onscreen: + /** + * Populate a cache entry from a caller identification (which got converted into a caller info). + */ + public static void populateCacheEntry(Context context, CallerInfo info, ContactCacheEntry cce, + int presentation, boolean isIncoming) { + Preconditions.checkNotNull(info); 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 @@ -219,21 +273,20 @@ public class ContactInfoCache implements CallerInfoAsyncQuery.OnQueryCompleteLis 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); - Log.d(this, " ==> no name *or* number! displayName = " + displayName); + displayName = getPresentationString(context, presentation); + Log.d(TAG, " ==> 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); - Log.d(this, " ==> presentation not allowed! displayName = " + displayName); + displayName = getPresentationString(context, presentation); + Log.d(TAG, " ==> 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; - Log.d(this, " ==> cnapName available: displayName '" - + displayName + "', displayNumber '" + displayNumber + "'"); + Log.d(TAG, " ==> 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, @@ -245,16 +298,16 @@ public class ContactInfoCache implements CallerInfoAsyncQuery.OnQueryCompleteLis // ...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)) { + if (isIncoming) { // 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 - Log.d(this, "Geodescrption: " + info.geoDescription); + Log.d(TAG, "Geodescrption: " + info.geoDescription); } - Log.d(this, " ==> no name; falling back to number: displayName '" - + displayName + "', displayNumber '" + displayNumber + "'"); + Log.d(TAG, + " ==> no name; falling back to number: displayName '" + displayName + "', displayNumber '" + displayNumber + "'"); } } else { // We do have a valid "name" in the CallerInfo. Display that @@ -263,79 +316,44 @@ public class ContactInfoCache implements CallerInfoAsyncQuery.OnQueryCompleteLis // 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); - Log.d(this, " ==> valid name, but presentation not allowed!" - + " displayName = " + displayName); + displayName = getPresentationString(context, presentation); + Log.d(TAG, + " ==> valid name, but presentation not allowed!" + " displayName = " + displayName); } else { displayName = info.name; displayNumber = number; label = info.phoneLabel; - Log.d(this, " ==> name is present in CallerInfo: displayName '" - + displayName + "', displayNumber '" + displayNumber + "'"); + Log.d(TAG, " ==> name is present in CallerInfo: displayName '" + displayName + + "', displayNumber '" + displayNumber + "'"); } } - personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, info.person_id); - Log.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) { - Log.v(this, "personUri is null. Just use unknown picture."); - photo = mContext.getResources().getDrawable(R.drawable.picture_unknown); - } else { - Log.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); + private void sendNotification(int callId, ContactCacheEntry entry) { + List callBacks = mCallBacks.get(callId); + if (callBacks != null) { + for (ContactInfoCacheCallback callBack : callBacks) { + callBack.onContactInfoComplete(callId, entry); + } } } /** * Gets name strings based on some special presentation modes. */ - private String getPresentationString(int presentation) { - String name = mContext.getString(R.string.unknown); + private static String getPresentationString(Context context, int presentation) { + String name = context.getString(R.string.unknown); if (presentation == Call.PRESENTATION_RESTRICTED) { - name = mContext.getString(R.string.private_num); + name = context.getString(R.string.private_num); } else if (presentation == Call.PRESENTATION_PAYPHONE) { - name = mContext.getString(R.string.payphone); + name = context.getString(R.string.payphone); } return name; } @@ -352,31 +370,15 @@ public class ContactInfoCache implements CallerInfoAsyncQuery.OnQueryCompleteLis public String number; public String label; public Drawable photo; - } - - private static class SearchEntry { - public Call call; - public boolean finished; - public final ContactCacheEntry info; - public final List callbacks = Lists.newArrayList(); - - public SearchEntry(Call call, ContactInfoCacheCallback callback) { - this.call = call; - - info = new ContactCacheEntry(); - finished = false; - callbacks.add(callback); - } - - public void addCallback(ContactInfoCacheCallback cb) { - if (!callbacks.contains(cb)) { - callbacks.add(cb); - } - } - public void finish() { - callbacks.clear(); - finished = true; + @Override + public String toString() { + return Objects.toStringHelper(this) + .add("name", MoreStrings.toSafeString(name)) + .add("number", MoreStrings.toSafeString(number)) + .add("label", label) + .add("photo", photo) + .toString(); } } } diff --git a/InCallUI/src/com/android/incallui/InCallActivity.java b/InCallUI/src/com/android/incallui/InCallActivity.java index ec5002f73..5915ecfb4 100644 --- a/InCallUI/src/com/android/incallui/InCallActivity.java +++ b/InCallUI/src/com/android/incallui/InCallActivity.java @@ -292,14 +292,11 @@ public class InCallActivity extends Activity { mainPresenter.getAudioModeProvider()); mCallButtonFragment.getPresenter().setProximitySensor( mainPresenter.getProximitySensor()); - mCallCardFragment.getPresenter().setAudioModeProvider( - mainPresenter.getAudioModeProvider()); - mCallCardFragment.getPresenter().setContactInfoCache( - mainPresenter.getContactInfoCache()); + final CallCardPresenter presenter = mCallCardFragment.getPresenter(); + presenter.setAudioModeProvider(mainPresenter.getAudioModeProvider()); mainPresenter.addListener(mCallButtonFragment.getPresenter()); mainPresenter.addListener(mCallCardFragment.getPresenter()); - mainPresenter.addListener(mAnswerFragment.getPresenter()); // setting activity should be last thing in setup process mainPresenter.setActivity(this); @@ -311,7 +308,6 @@ public class InCallActivity extends Activity { mainPresenter.removeListener(mCallButtonFragment.getPresenter()); mainPresenter.removeListener(mCallCardFragment.getPresenter()); - mainPresenter.removeListener(mAnswerFragment.getPresenter()); mainPresenter.setActivity(null); } diff --git a/InCallUI/src/com/android/incallui/InCallPresenter.java b/InCallUI/src/com/android/incallui/InCallPresenter.java index 8bb0973bf..3aa717c62 100644 --- a/InCallUI/src/com/android/incallui/InCallPresenter.java +++ b/InCallUI/src/com/android/incallui/InCallPresenter.java @@ -23,7 +23,9 @@ import android.content.Context; import android.content.Intent; import com.android.services.telephony.common.Call; +import com.google.common.collect.Lists; +import java.util.ArrayList; import java.util.Set; /** @@ -40,6 +42,7 @@ public class InCallPresenter implements CallList.Listener { private static InCallPresenter sInCallPresenter; private final Set mListeners = Sets.newHashSet(); + private final ArrayList mIncomingCallListeners = Lists.newArrayList(); private AudioModeProvider mAudioModeProvider; private StatusBarNotifier mStatusBarNotifier; @@ -65,10 +68,11 @@ public class InCallPresenter implements CallList.Listener { mCallList = callList; mCallList.addListener(this); - mContactInfoCache = new ContactInfoCache(context); + mContactInfoCache = ContactInfoCache.getInstance(context); mStatusBarNotifier = new StatusBarNotifier(context, mContactInfoCache, mCallList); addListener(mStatusBarNotifier); + addIncomingCallListener(mStatusBarNotifier); mAudioModeProvider = audioModeProvider; @@ -137,6 +141,20 @@ public class InCallPresenter implements CallList.Listener { } } + /** + * Called when there is a new incoming call. + * + * @param call + */ + @Override + public void onIncomingCall(Call call) { + mInCallState = startOrFinishUi(InCallState.INCOMING); + + for (IncomingCallListener listener : mIncomingCallListeners) { + listener.onIncomingCall(call); + } + } + /** * Given the call list, return the state in which the in-call screen should be. */ @@ -156,6 +174,11 @@ public class InCallPresenter implements CallList.Listener { return newState; } + public void addIncomingCallListener(IncomingCallListener listener) { + Preconditions.checkNotNull(listener); + mIncomingCallListeners.add(listener); + } + public void addListener(InCallStateListener listener) { Preconditions.checkNotNull(listener); mListeners.add(listener); @@ -375,4 +398,8 @@ public class InCallPresenter implements CallList.Listener { // TODO(klp): Enhance state to contain the call objects instead of passing CallList public void onStateChange(InCallState state, CallList callList); } + + public interface IncomingCallListener { + public void onIncomingCall(Call call); + } } diff --git a/InCallUI/src/com/android/incallui/StatusBarNotifier.java b/InCallUI/src/com/android/incallui/StatusBarNotifier.java index 466c7b214..f8289c90b 100644 --- a/InCallUI/src/com/android/incallui/StatusBarNotifier.java +++ b/InCallUI/src/com/android/incallui/StatusBarNotifier.java @@ -16,6 +16,7 @@ package com.android.incallui; +import com.android.services.telephony.common.CallIdentification; import com.google.common.base.Preconditions; import android.app.Notification; @@ -37,7 +38,7 @@ import com.android.services.telephony.common.Call; * This class adds Notifications to the status bar for the in-call experience. */ public class StatusBarNotifier implements InCallPresenter.InCallStateListener, - ContactInfoCacheCallback { + InCallPresenter.IncomingCallListener { // notification types private static final int IN_CALL_NOTIFICATION = 1; @@ -71,12 +72,25 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener, updateNotification(state, callList); } - /** - * Called after the Contact Info query has finished. - */ @Override - public void onContactInfoComplete(int callId, ContactCacheEntry entry) { - updateNotification(mInCallState, mCallList); + public void onIncomingCall(final Call call) { + final ContactCacheEntry entry = ContactInfoCache.buildCacheEntryFromCall(mContext, + call.getIdentification(), true); + + // Initial update with no contact information. + buildAndSendNotification(InCallState.INCOMING, call, entry, false); + + // 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.getIdentification(), true, new ContactInfoCacheCallback() { + @Override + public void onContactInfoComplete(int callId, ContactCacheEntry entry) { + buildAndSendNotification(InCallState.INCOMING, call, entry, false); + } + }); } /** @@ -160,28 +174,24 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener, Log.d(this, "updateInCallNotification(allowFullScreenIntent = " + allowFullScreenIntent + ")..."); - if (shouldSuppressNotification(state, callList)) { + final Call call = getCallToShow(callList); + if (shouldSuppressNotification(state, call)) { cancelInCall(); return; } - final Call call = getCallToShow(callList); if (call == null) { Log.wtf(this, "No call for the notification!"); + return; } - // 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); - } - }); - + // Contact info should have already been done on incoming calls. + ContactCacheEntry entry = mContactInfoCache.getInfo(call.getCallId()); + if (entry == null) { + entry = ContactInfoCache.buildCacheEntryFromCall(mContext, call.getIdentification(), + state == InCallState.INCOMING); + } + buildAndSendNotification(state, call, entry, allowFullScreenIntent); } /** @@ -426,7 +436,7 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener, /** * Returns true if notification should not be shown in the current state. */ - private boolean shouldSuppressNotification(InCallState state, CallList callList) { + private boolean shouldSuppressNotification(InCallState state, Call call) { // 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* @@ -440,7 +450,6 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener, // 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; } -- cgit v1.2.3