Merge "Clean up listeners when view is destroyed." into klp-dev
diff --git a/InCallUI/res/layout/answer_fragment.xml b/InCallUI/res/layout/answer_fragment.xml
index 05e83de..d62a8d5 100644
--- a/InCallUI/res/layout/answer_fragment.xml
+++ b/InCallUI/res/layout/answer_fragment.xml
@@ -27,9 +27,9 @@
         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"
-        dc:directionDescriptions="@array/incoming_call_widget_2way_direction_descriptions"
+        dc:targetDrawables="@array/incoming_call_widget_3way_targets"
+        dc:targetDescriptions="@array/incoming_call_widget_3way_target_descriptions"
+        dc:directionDescriptions="@array/incoming_call_widget_3way_direction_descriptions"
         dc:handleDrawable="@drawable/ic_in_call_touch_handle"
         dc:outerRingDrawable="@*android:drawable/ic_lockscreen_outerring"
         dc:outerRadius="@dimen/glowpadview_target_placement_radius"
diff --git a/InCallUI/src/com/android/incallui/AnswerFragment.java b/InCallUI/src/com/android/incallui/AnswerFragment.java
index 40462ce..2429dac 100644
--- a/InCallUI/src/com/android/incallui/AnswerFragment.java
+++ b/InCallUI/src/com/android/incallui/AnswerFragment.java
@@ -47,6 +47,8 @@
 
     private ArrayAdapter<String> mTextResponsesAdapter = null;
 
+    private GlowPadWrapper mGlowpad;
+
     public AnswerFragment() {
     }
 
@@ -63,18 +65,12 @@
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
             Bundle savedInstanceState) {
-        final GlowPadWrapper glowPad = (GlowPadWrapper) inflater.inflate(R.layout.answer_fragment,
+        mGlowpad = (GlowPadWrapper) inflater.inflate(R.layout.answer_fragment,
                 container, false);
 
-        glowPad.setAnswerListener(this);
+        mGlowpad.setAnswerListener(this);
 
-        return glowPad;
-    }
-
-    @Override
-    public void onDestroyView() {
-        super.onDestroyView();
-        getPresenter().onUiUnready(this);
+        return mGlowpad;
     }
 
     @Override
@@ -84,7 +80,29 @@
 
     @Override
     public void showTextButton(boolean show) {
-        // TODO(klp) Hide the text button when the call does not support reject by text.
+        final int targetResourceId = show
+                ? R.array.incoming_call_widget_3way_targets
+                : R.array.incoming_call_widget_2way_targets;
+
+        if (targetResourceId != mGlowpad.getTargetResourceId()) {
+            if (show) {
+                // Answer, Decline, and Respond via SMS.
+                mGlowpad.setTargetResources(targetResourceId);
+                mGlowpad.setTargetDescriptionsResourceId(
+                        R.array.incoming_call_widget_3way_target_descriptions);
+                mGlowpad.setDirectionDescriptionsResourceId(
+                        R.array.incoming_call_widget_3way_direction_descriptions);
+            } else {
+                // Answer or Decline.
+                mGlowpad.setTargetResources(targetResourceId);
+                mGlowpad.setTargetDescriptionsResourceId(
+                        R.array.incoming_call_widget_2way_target_descriptions);
+                mGlowpad.setDirectionDescriptionsResourceId(
+                        R.array.incoming_call_widget_2way_direction_descriptions);
+            }
+
+            mGlowpad.reset(false);
+        }
     }
 
     @Override
diff --git a/InCallUI/src/com/android/incallui/AnswerPresenter.java b/InCallUI/src/com/android/incallui/AnswerPresenter.java
index 40b2a36..4c0f1d6 100644
--- a/InCallUI/src/com/android/incallui/AnswerPresenter.java
+++ b/InCallUI/src/com/android/incallui/AnswerPresenter.java
@@ -85,7 +85,7 @@
                 call.getCallId());
         getUi().showAnswerUi(true);
 
-        if (textMsgs != null) {
+        if (call.can(Call.Capabilities.RESPOND_VIA_TEXT) && textMsgs != null) {
             getUi().showTextButton(true);
             getUi().configureMessageDialogue(textMsgs);
         } else {
@@ -127,14 +127,13 @@
     }
 
     public void onText() {
-        // No-op for now.  b/10424370
-        // getUi().showMessageDialogue();
+        getUi().showMessageDialogue();
     }
 
     public void rejectCallWithMessage(String message) {
         Log.d(this, "sendTextToDefaultActivity()...");
-        CallCommandClient.getInstance().rejectCall(mCallId, true, message);
         getUi().dismissPopup();
+        CallCommandClient.getInstance().rejectCall(mCallId, true, message);
     }
 
     interface AnswerUi extends Ui {
diff --git a/InCallUI/src/com/android/incallui/AudioModeProvider.java b/InCallUI/src/com/android/incallui/AudioModeProvider.java
index 36ec20f..8224d3e 100644
--- a/InCallUI/src/com/android/incallui/AudioModeProvider.java
+++ b/InCallUI/src/com/android/incallui/AudioModeProvider.java
@@ -84,6 +84,10 @@
         return mAudioMode;
     }
 
+    public boolean getMute() {
+        return mMuted;
+    }
+
     /* package */ interface AudioModeListener {
         void onAudioMode(int newMode);
         void onMute(boolean muted);
diff --git a/InCallUI/src/com/android/incallui/BaseFragment.java b/InCallUI/src/com/android/incallui/BaseFragment.java
index a348ce4..ae207f3 100644
--- a/InCallUI/src/com/android/incallui/BaseFragment.java
+++ b/InCallUI/src/com/android/incallui/BaseFragment.java
@@ -18,12 +18,9 @@
 
 import android.app.Fragment;
 import android.os.Bundle;
-import android.view.View;
-
-import com.android.internal.util.Preconditions;
 
 /**
- *
+ * Parent for all fragments that use Presenters and Ui design.
  */
 public abstract class BaseFragment<T extends Presenter<U>, U extends Ui> extends Fragment {
 
@@ -51,4 +48,10 @@
         super.onActivityCreated(savedInstanceState);
         mPresenter.onUiReady(getUi());
     }
+
+    @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+        mPresenter.onUiUnready(getUi());
+    }
 }
diff --git a/InCallUI/src/com/android/incallui/CallButtonFragment.java b/InCallUI/src/com/android/incallui/CallButtonFragment.java
index bd19381..1c3e737 100644
--- a/InCallUI/src/com/android/incallui/CallButtonFragment.java
+++ b/InCallUI/src/com/android/incallui/CallButtonFragment.java
@@ -127,12 +127,6 @@
     }
 
     @Override
-    public void onDestroyView() {
-        super.onDestroyView();
-        getPresenter().onUiUnready(this);
-    }
-
-    @Override
     public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
     }
 
diff --git a/InCallUI/src/com/android/incallui/CallButtonPresenter.java b/InCallUI/src/com/android/incallui/CallButtonPresenter.java
index 6e64e65..bc9efe5 100644
--- a/InCallUI/src/com/android/incallui/CallButtonPresenter.java
+++ b/InCallUI/src/com/android/incallui/CallButtonPresenter.java
@@ -32,6 +32,8 @@
     private Call mCall;
     private AudioModeProvider mAudioModeProvider;
     private ProximitySensor mProximitySensor;
+    private boolean mAutomaticallyMuted = false;
+    private boolean mPreviousMuteState = false;
 
     public CallButtonPresenter() {
     }
@@ -166,6 +168,11 @@
     }
 
     public void addCallClicked() {
+        // Automatically mute the current call
+        mAutomaticallyMuted = true;
+        mPreviousMuteState = mAudioModeProvider.getMute();
+        getUi().setMute(true);
+
         CallCommandClient.getInstance().addCall();
     }
 
@@ -204,6 +211,12 @@
             ui.showMerge(call.can(Capabilities.MERGE_CALLS));
             ui.showSwap(call.can(Capabilities.SWAP_CALLS));
             ui.showAddCall(call.can(Capabilities.ADD_CALL));
+
+            // Restore the previous mute state
+            if (mAutomaticallyMuted && mAudioModeProvider.getMute() != mPreviousMuteState) {
+                ui.setMute(mPreviousMuteState);
+                mAutomaticallyMuted = false;
+            }
         }
     }
 
diff --git a/InCallUI/src/com/android/incallui/CallCardFragment.java b/InCallUI/src/com/android/incallui/CallCardFragment.java
index d880b97..6009036 100644
--- a/InCallUI/src/com/android/incallui/CallCardFragment.java
+++ b/InCallUI/src/com/android/incallui/CallCardFragment.java
@@ -111,12 +111,6 @@
     }
 
     @Override
-    public void onDestroyView() {
-        super.onDestroyView();
-        getPresenter().onUiUnready(this);
-    }
-
-    @Override
     public void setVisible(boolean on) {
         if (on) {
             getView().setVisibility(View.VISIBLE);
diff --git a/InCallUI/src/com/android/incallui/CallCardPresenter.java b/InCallUI/src/com/android/incallui/CallCardPresenter.java
index 59e846b..a0ad3ce 100644
--- a/InCallUI/src/com/android/incallui/CallCardPresenter.java
+++ b/InCallUI/src/com/android/incallui/CallCardPresenter.java
@@ -74,6 +74,8 @@
 
         // Call may be null if disconnect happened already.
         if (call != null) {
+            mPrimary = call;
+
             final CallIdentification identification = call.getIdentification();
 
             // TODO(klp): Logic to determine which ui field get what data resides in
@@ -241,7 +243,7 @@
             //   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.");
+            Log.d(TAG, "Contact lookup. In memory cache miss. Searching provider.");
             cache.findInfo(identification, isIncoming, new ContactInfoCacheCallback() {
                 @Override
                 public void onContactInfoComplete(int callId, ContactCacheEntry entry) {
@@ -250,16 +252,16 @@
                     // 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.");
+                        Log.d(TAG, "Contact lookup. Contact provider miss. Searching people api.");
                         lookupPhoneNumber(identification.getNumber());
                     } else {
-                        Log.d(TAG, "Found contact in provider: " + entry);
+                        Log.d(TAG, "Contact lookup. Found in contact provider: " + entry);
                         updateContactEntry(entry, isPrimary, isConference);
                     }
                 }
             });
         } else {
-            Log.d(TAG, "Found contact in cache: " + entry);
+            Log.d(TAG, "Contact lookup. Found in memory cache: " + entry);
             updateContactEntry(entry, isPrimary, isConference);
         }
     }
diff --git a/InCallUI/src/com/android/incallui/ContactInfoCache.java b/InCallUI/src/com/android/incallui/ContactInfoCache.java
index 1895989..0796794 100644
--- a/InCallUI/src/com/android/incallui/ContactInfoCache.java
+++ b/InCallUI/src/com/android/incallui/ContactInfoCache.java
@@ -36,14 +36,12 @@
 
 import java.util.HashMap;
 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.
+ * 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 ContactsAsyncHelper.OnImageLoadCompleteListener {
 
@@ -52,7 +50,9 @@
 
     private final Context mContext;
     private final HashMap<Integer, ContactCacheEntry> mInfoMap = Maps.newHashMap();
-    private final HashMap<Integer, List<ContactInfoCacheCallback>> mCallBacks = Maps.newHashMap();
+    private final HashMap<Integer, List<ContactInfoCacheCallback>> mCallBacksGuarded = Maps
+            .newHashMap();
+    private final Object mCallBackLock = new Object();
 
     private static ContactInfoCache sCache = null;
 
@@ -95,46 +95,48 @@
         Preconditions.checkState(Looper.getMainLooper().getThread() == Thread.currentThread());
         Preconditions.checkNotNull(callback);
 
-        // TODO(klp): We dont need to make this call if the call Id already exists in mInfoMap.
         final int callId = identification.getCallId();
         // If the entry already exists, add callback
-        List<ContactInfoCacheCallback> 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 {
-            callBacks.add(callback);
+        List<ContactInfoCacheCallback> callBacks;
+        synchronized (mCallBackLock) {
+            callBacks = mCallBacksGuarded.get(callId);
+            if (callBacks != null) {
+                callBacks.add(callback);
+                return;
+            } else {
+                // New lookup
+                callBacks = Lists.newArrayList();
+                callBacks.add(callback);
+                mCallBacksGuarded.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);
+                    }
+                });
+
     }
 
     /**
@@ -179,8 +181,10 @@
      * Blows away the stored cache values.
      */
     public void clearCache() {
-        mInfoMap.clear();
-        mCallBacks.clear();
+        synchronized (mCallBackLock) {
+            mInfoMap.clear();
+            mCallBacksGuarded.clear();
+        }
     }
 
     private static ContactCacheEntry buildEntry(Context context, int callId,
@@ -338,7 +342,12 @@
      * Sends the updated information to call the callbacks for the entry.
      */
     private void sendNotification(int callId, ContactCacheEntry entry) {
-        List<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
+        final List<ContactInfoCacheCallback> callBacks;
+        synchronized (mCallBackLock) {
+            callBacks = mCallBacksGuarded.get(callId);
+            // Do not clear mInfoMap here because we still need the data.
+            mCallBacksGuarded.clear();
+        }
         if (callBacks != null) {
             for (ContactInfoCacheCallback callBack : callBacks) {
                 callBack.onContactInfoComplete(callId, entry);