Add UI to show when the Call disconnects.

When the call disconnects, we need to show UI for a short time before
existing the UI.

Change-Id: Iee648b8c54ee4b5ea09cfaec508e1bf8fb8f1643
diff --git a/InCallUI/res/values/strings.xml b/InCallUI/res/values/strings.xml
index 2ecc842..882102a 100755
--- a/InCallUI/res/values/strings.xml
+++ b/InCallUI/res/values/strings.xml
@@ -32,6 +32,55 @@
     <!-- Incoming call screen, string when called from a pay phone -->
     <string name="payphone">Pay phone</string>
 
+    <!-- In-call screen: call failure reason (busy) -->
+    <string name="callFailed_userBusy">Line busy</string>
+    <!-- In-call screen: call failure reason (network congestion) -->
+    <string name="callFailed_congestion">Network busy</string>
+    <!-- In-call screen: call failure reason (client timed out) -->
+    <string name="callFailed_timedOut">No response, timed out</string>
+    <!-- In-call screen: call failure reason (server unreachable) -->
+    <string name="callFailed_server_unreachable">Server unreachable</string>
+    <!-- In-call screen: call failure reason (peer unreachable) -->
+    <string name="callFailed_number_unreachable">Number unreachable</string>
+    <!-- In-call screen: call failure reason (incorrect username or password) -->
+    <string name="callFailed_invalid_credentials">Incorrect username or password</string>
+    <!-- In-call screen: call failure reason (calling from out of network is not allowed) -->
+    <string name="callFailed_out_of_network">Called from out-of-network</string>
+    <!-- In-call screen: call failure reason (server error) -->
+    <string name="callFailed_server_error">Server error. Try again later.</string>
+    <!-- In-call screen: call failure reason (no signal) -->
+    <string name="callFailed_noSignal">No signal</string>
+    <!-- In-call screen: call failure reason (GSM ACM limit exceeded) -->
+    <string name="callFailed_limitExceeded">ACM limit exceeded</string>
+    <!-- In-call screen: call failure reason (radio is off) -->
+    <string name="callFailed_powerOff">Radio off</string>
+    <!-- In-call screen: call failure reason (SIM error) -->
+    <string name="callFailed_simError">No SIM or SIM error</string>
+    <!-- In-call screen: call failure reason (out of service) -->
+    <string name="callFailed_outOfService">Out of service area</string>
+    <!-- In-call screen: call failure reason (call denied because of current FDN setting) -->
+    <string name="callFailed_fdn_only">Outgoing calls are restricted by FDN.</string>
+    <!-- In-call screen: call failure reason (call denied because call barring is on) -->
+    <string name="callFailed_cb_enabled">You can\'t make outgoing calls while call barring is on.</string>
+    <!-- In-call screen: call failure reason (call denied because domain specific access control is on) -->
+    <string name="callFailed_dsac_restricted">All calls are restricted by access control.</string>
+    <!-- In-call screen: call failure reason (Emergency call denied because domain specific access control is on)-->
+    <string name="callFailed_dsac_restricted_emergency">Emergency calls are restricted by access control.</string>
+    <!-- In-call screen: call failure reason (Normal call denied because domain specific access control is on)-->
+    <string name="callFailed_dsac_restricted_normal">Normal calls are restricted by access control.</string>
+    <!-- In-call screen: call failure reason (Dialed number doesn't exist) -->
+    <string name="callFailed_unobtainable_number">Invalid number</string>
+    <!-- In-call screen: status label for a conference call -->
+    <string name="confCall">Conference call</string>
+    <!-- In-call screen: call lost dialog text -->
+    <string name="call_lost">Call has been lost.</string>
+
+    <!-- Positive button label ("OK") used in several dialogs in the phone UI [CHAR LIMIT=10] -->
+    <string name="ok">OK</string>
+
+    <!-- MMI dialog strings -->
+    <!-- Dialog label when an MMI code starts running -->
+
     <!-- "Audio mode" popup menu: Item label to select the speakerphone [CHAR LIMIT=25] -->
     <string name="audio_mode_speaker">Speaker</string>
     <!-- "Audio mode" popup menu: Item label to select the handset earpiece [CHAR LIMIT=25] -->
diff --git a/InCallUI/src/com/android/incallui/AnswerFragment.java b/InCallUI/src/com/android/incallui/AnswerFragment.java
index 79247c5..d1a1df0 100644
--- a/InCallUI/src/com/android/incallui/AnswerFragment.java
+++ b/InCallUI/src/com/android/incallui/AnswerFragment.java
@@ -70,6 +70,7 @@
 
     @Override
     public void onDestroyView() {
+        super.onDestroyView();
         getPresenter().onUiUnready(this);
     }
 
diff --git a/InCallUI/src/com/android/incallui/CallButtonFragment.java b/InCallUI/src/com/android/incallui/CallButtonFragment.java
index 175fe4d..1b9465d 100644
--- a/InCallUI/src/com/android/incallui/CallButtonFragment.java
+++ b/InCallUI/src/com/android/incallui/CallButtonFragment.java
@@ -119,6 +119,7 @@
 
     @Override
     public void onDestroyView() {
+        super.onDestroyView();
         getPresenter().onUiUnready(this);
     }
 
diff --git a/InCallUI/src/com/android/incallui/CallCardFragment.java b/InCallUI/src/com/android/incallui/CallCardFragment.java
index d4782c5..19c32f4 100644
--- a/InCallUI/src/com/android/incallui/CallCardFragment.java
+++ b/InCallUI/src/com/android/incallui/CallCardFragment.java
@@ -22,6 +22,7 @@
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.text.TextUtils;
+import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -29,6 +30,8 @@
 import android.widget.ImageView;
 import android.widget.TextView;
 
+import com.android.services.telephony.common.Call;
+
 /**
  * Fragment for call card.
  */
@@ -39,8 +42,7 @@
     private TextView mNumberLabel;
     private TextView mName;
     private ImageView mPhoto;
-
-
+    private TextView mCallStateLabel;
     private ViewStub mSecondaryCallInfo;
     private TextView mSecondaryCallName;
 
@@ -63,6 +65,7 @@
         mNumberLabel = (TextView) view.findViewById(R.id.label);
         mSecondaryCallInfo = (ViewStub) view.findViewById(R.id.secondary_call_info);
         mPhoto = (ImageView) view.findViewById(R.id.photo);
+        mCallStateLabel = (TextView) view.findViewById(R.id.callStateLabel);
 
         // This method call will begin the callbacks on CallCardUi. We need to ensure
         // everything needed for the callbacks is set up before this is called.
@@ -71,6 +74,7 @@
 
     @Override
     public void onDestroyView() {
+        super.onDestroyView();
         getPresenter().onUiUnready(this);
     }
 
@@ -139,6 +143,109 @@
         }
     }
 
+    @Override
+    public void setCallState(int state, Call.DisconnectCause cause) {
+        String callStateLabel = null;
+
+        // States other than disconnected not yet supported
+        if (state == Call.State.DISCONNECTED) {
+            callStateLabel = getCallFailedString(cause);
+        }
+
+        Logger.d(this, "setCallState ", callStateLabel);
+
+        if (!TextUtils.isEmpty(callStateLabel)) {
+            mCallStateLabel.setVisibility(View.VISIBLE);
+            mCallStateLabel.setText(callStateLabel);
+        } else {
+            mCallStateLabel.setVisibility(View.GONE);
+            // Gravity is aligned left when receiving an incoming call in landscape.
+            // In that rare case, the gravity needs to be reset to the right.
+            // Also, setText("") is used since there is a delay in making the view GONE,
+            // so the user will otherwise see the text jump to the right side before disappearing.
+            if(mCallStateLabel.getGravity() != Gravity.END) {
+                mCallStateLabel.setText("");
+                mCallStateLabel.setGravity(Gravity.END);
+            }
+        }
+    }
+
+    /**
+     * Maps the disconnect cause to a resource string.
+     */
+    private String getCallFailedString(Call.DisconnectCause cause) {
+        int resID = R.string.card_title_call_ended;
+
+        // TODO: The card *title* should probably be "Call ended" in all
+        // cases, but if the DisconnectCause was an error condition we should
+        // probably also display the specific failure reason somewhere...
+
+        switch (cause) {
+            case BUSY:
+                resID = R.string.callFailed_userBusy;
+                break;
+
+            case CONGESTION:
+                resID = R.string.callFailed_congestion;
+                break;
+
+            case TIMED_OUT:
+                resID = R.string.callFailed_timedOut;
+                break;
+
+            case SERVER_UNREACHABLE:
+                resID = R.string.callFailed_server_unreachable;
+                break;
+
+            case NUMBER_UNREACHABLE:
+                resID = R.string.callFailed_number_unreachable;
+                break;
+
+            case INVALID_CREDENTIALS:
+                resID = R.string.callFailed_invalid_credentials;
+                break;
+
+            case SERVER_ERROR:
+                resID = R.string.callFailed_server_error;
+                break;
+
+            case OUT_OF_NETWORK:
+                resID = R.string.callFailed_out_of_network;
+                break;
+
+            case LOST_SIGNAL:
+            case CDMA_DROP:
+                resID = R.string.callFailed_noSignal;
+                break;
+
+            case LIMIT_EXCEEDED:
+                resID = R.string.callFailed_limitExceeded;
+                break;
+
+            case POWER_OFF:
+                resID = R.string.callFailed_powerOff;
+                break;
+
+            case ICC_ERROR:
+                resID = R.string.callFailed_simError;
+                break;
+
+            case OUT_OF_SERVICE:
+                resID = R.string.callFailed_outOfService;
+                break;
+
+            case INVALID_NUMBER:
+            case UNOBTAINABLE_NUMBER:
+                resID = R.string.callFailed_unobtainable_number;
+                break;
+
+            default:
+                resID = R.string.card_title_call_ended;
+                break;
+        }
+        return this.getView().getContext().getString(resID);
+    }
+
     private void showAndInitializeSecondaryCallInfo() {
         mSecondaryCallInfo.setVisibility(View.VISIBLE);
 
diff --git a/InCallUI/src/com/android/incallui/CallCardPresenter.java b/InCallUI/src/com/android/incallui/CallCardPresenter.java
index 5f67a51..d97c22e 100644
--- a/InCallUI/src/com/android/incallui/CallCardPresenter.java
+++ b/InCallUI/src/com/android/incallui/CallCardPresenter.java
@@ -75,17 +75,12 @@
         } else if (state == InCallState.OUTGOING) {
             primary = callList.getOutgoingCall();
 
-            // Safe to assume that the primary call is valid since we would not be in the
-            // OUTGOING state without an outgoing call.
-            secondary = callList.getBackgroundCall();
+            // getCallToDisplay doesn't go through outgoing or incoming calls. It will return the
+            // highest priority call to display as the secondary call.
+            secondary = getCallToDisplay(callList, null);
         } else if (state == InCallState.INCALL) {
-            primary = callList.getActiveCall();
-            if (primary != null) {
-                secondary = callList.getBackgroundCall();
-            } else {
-                primary = callList.getBackgroundCall();
-                secondary = callList.getSecondBackgroundCall();
-            }
+            primary = getCallToDisplay(callList, null);
+            secondary = getCallToDisplay(callList, primary);
         }
 
         Logger.d(this, "Primary call: " + primary);
@@ -96,6 +91,14 @@
                 null, this);
         updateDisplayByCallerInfo(primary, primaryCallInfo, primary.getNumberPresentation(), true);
 
+        if (primary != null) {
+            ui.setNumber(primary.getNumber());
+            ui.setCallState(primary.getState(), primary.getDisconnectCause());
+        } else {
+            ui.setNumber("");
+            ui.setCallState(Call.State.INVALID, Call.DisconnectCause.UNKNOWN);
+        }
+
         // Set secondary call data
         if (secondary != null) {
             ui.setSecondaryCallInfo(true, secondary.getNumber());
@@ -104,6 +107,41 @@
         }
     }
 
+    /**
+     * 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
+     * by passing a previously found primary call as ignore.
+     *
+     * @param ignore A call to ignore if found.
+     */
+    private Call getCallToDisplay(CallList callList, Call ignore) {
+
+        // Disconnected calls get primary position to let user know quickly
+        // what call has disconnected. Disconnected calls are very short lived.
+        Call retval = callList.getDisconnectedCall();
+        if (retval != null && retval != ignore) {
+            return retval;
+        }
+
+        // Active calls come second.  An active call always gets precedent.
+        retval = callList.getActiveCall();
+        if (retval != null && retval != ignore) {
+            return retval;
+        }
+
+        // Then we go to background call (calls on hold)
+        retval = callList.getBackgroundCall();
+        if (retval != null && retval != ignore) {
+            return retval;
+        }
+
+        // Lastly, we go to a second background call.
+        retval = callList.getSecondBackgroundCall();
+
+        return retval;
+    }
+
     public interface CallCardUi extends Ui {
         // TODO(klp): Consider passing in the Call object directly in these methods.
         void setVisible(boolean on);
@@ -115,6 +153,7 @@
         void setImage(Drawable drawable);
         void setImage(Bitmap bitmap);
         void setSecondaryCallInfo(boolean show, String number);
+        void setCallState(int state, Call.DisconnectCause cause);
     }
 
     @Override
diff --git a/InCallUI/src/com/android/incallui/CallHandlerService.java b/InCallUI/src/com/android/incallui/CallHandlerService.java
index f0d98b2..0b2be56 100644
--- a/InCallUI/src/com/android/incallui/CallHandlerService.java
+++ b/InCallUI/src/com/android/incallui/CallHandlerService.java
@@ -42,6 +42,7 @@
     private static final int ON_UPDATE_CALL_WITH_TEXT_RESPONSES = 3;
     private static final int ON_AUDIO_MODE = 4;
     private static final int ON_SUPPORTED_AUDIO_MODE = 5;
+    private static final int ON_DISCONNECT_CALL = 6;
 
 
     private CallList mCallList;
@@ -79,7 +80,7 @@
         @Override
         public void onDisconnect(Call call) {
             Logger.d(CallHandlerService.this, "onDisconnected");
-            mMainHandler.sendMessage(mMainHandler.obtainMessage(ON_UPDATE_CALL, 0, 0, call));
+            mMainHandler.sendMessage(mMainHandler.obtainMessage(ON_DISCONNECT_CALL, 0, 0, call));
         }
 
         @Override
@@ -149,6 +150,9 @@
             case ON_UPDATE_CALL_WITH_TEXT_RESPONSES:
                 mCallList.onUpdate((AbstractMap.SimpleEntry<Call, List<String> >) msg.obj);
                 break;
+            case ON_DISCONNECT_CALL:
+                mCallList.onDisconnect((Call) msg.obj);
+                break;
             case ON_AUDIO_MODE:
                 mAudioModeProvider.onAudioModeChange(msg.arg1);
                 break;
diff --git a/InCallUI/src/com/android/incallui/CallList.java b/InCallUI/src/com/android/incallui/CallList.java
index 5ea4fc5..bd631ce 100644
--- a/InCallUI/src/com/android/incallui/CallList.java
+++ b/InCallUI/src/com/android/incallui/CallList.java
@@ -23,6 +23,9 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 
+import android.os.Handler;
+import android.os.Message;
+
 import com.android.services.telephony.common.Call;
 
 import java.util.AbstractMap;
@@ -39,6 +42,10 @@
  */
 public class CallList {
 
+    private static final int DISCONNECTED_CALL_TIMEOUT_MS = 3000;
+
+    private static final int EVENT_DISCONNECTED_TIMEOUT = 1;
+
     private static CallList sInstance;
 
     private final HashMap<Integer, Call> mCallMap = Maps.newHashMap();
@@ -66,7 +73,17 @@
      * Called when a single call has changed.
      */
     public void onUpdate(Call call) {
-        Logger.d(this, "onUpdate - " + call);
+        Logger.d(this, "onUpdate - ", call);
+
+        updateCallInMap(call);
+        notifyListenersOfChange();
+    }
+
+    /**
+     * Called when a single call disconnects.
+     */
+    public void onDisconnect(Call call) {
+        Logger.d(this, "onDisconnect: ", call);
 
         updateCallInMap(call);
 
@@ -141,6 +158,10 @@
         return getFirstCallWithState(Call.State.ONHOLD);
     }
 
+    public Call getDisconnectedCall() {
+        return getFirstCallWithState(Call.State.DISCONNECTED);
+    }
+
     public Call getSecondBackgroundCall() {
         return getCallWithState(Call.State.ONHOLD, 1);
     }
@@ -200,7 +221,7 @@
             }
         }
 
-        Logger.d(this, "Found call: " + retval);
+        Logger.v(this, "Found call: ", retval);
         return retval;
     }
 
@@ -219,7 +240,21 @@
 
         final Integer id = new Integer(call.getCallId());
 
-        if (!isCallDead(call)) {
+        if (call.getState() == Call.State.DISCONNECTED) {
+            // For disconnected calls, we want to keep them alive for a few seconds so that the UI
+            // has a chance to display anything it needs when a call is disconnected.
+
+            // Set up a timer to destroy the call after X seconds.
+            Message msg = mHandler.obtainMessage(EVENT_DISCONNECTED_TIMEOUT, call);
+            boolean sent = mHandler.sendMessageDelayed(msg, DISCONNECTED_CALL_TIMEOUT_MS);
+
+            Logger.d(this, "Retval from sendMessageDelayed: ", Boolean.toString(sent));
+
+            // Don't add disconnected calls that do not already exist in the map
+            if (mCallMap.containsKey(id)) {
+                mCallMap.put(id, call);
+            }
+        } else if (!isCallDead(call)) {
             mCallMap.put(id, call);
         } else if (mCallMap.containsKey(id)) {
             mCallMap.remove(id);
@@ -246,6 +281,33 @@
     }
 
     /**
+     * Sets up a call for deletion and notifies listeners of change.
+     */
+    private void finishDisconnectedCall(Call call) {
+        call.setState(Call.State.IDLE);
+        updateCallInMap(call);
+        notifyListenersOfChange();
+    }
+
+    /**
+     * Handles the timeout for destroying disconnected calls.
+     */
+    private Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case EVENT_DISCONNECTED_TIMEOUT:
+                    Logger.d(this, "EVENT_DISCONNECTED_TIMEOUT ", msg.obj);
+                    finishDisconnectedCall((Call) msg.obj);
+                    break;
+                default:
+                    Logger.wtf(this, "Message not expected: " + msg.what);
+                    break;
+            }
+        }
+    };
+
+    /**
      * Listener interface for any class that wants to be notified of changes
      * to the call list.
      */
diff --git a/InCallUI/src/com/android/incallui/InCallPresenter.java b/InCallUI/src/com/android/incallui/InCallPresenter.java
index 72a880a..63f0aa5 100644
--- a/InCallUI/src/com/android/incallui/InCallPresenter.java
+++ b/InCallUI/src/com/android/incallui/InCallPresenter.java
@@ -105,7 +105,8 @@
         } else if (callList.getOutgoingCall() != null) {
             newState = InCallState.OUTGOING;
         } else if (callList.getActiveCall() != null ||
-                callList.getBackgroundCall() != null) {
+                callList.getBackgroundCall() != null ||
+                callList.getDisconnectedCall() != null) {
             newState = InCallState.INCALL;
         }
 
diff --git a/InCallUI/src/com/android/incallui/Logger.java b/InCallUI/src/com/android/incallui/Logger.java
index 5f628d2..e7cbe20 100644
--- a/InCallUI/src/com/android/incallui/Logger.java
+++ b/InCallUI/src/com/android/incallui/Logger.java
@@ -41,9 +41,9 @@
         }
     }
 
-    public static void v(String tag, String msg) {
-        if (VERBOSE) {
-            Log.v(TAG, tag + msg);
+    public static void d(Object obj, String str1, Object str2) {
+        if (DEBUG) {
+            Log.d(TAG, getPrefix(obj) + str1 + str2);
         }
     }
 
@@ -61,6 +61,12 @@
         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);
     }
@@ -77,6 +83,10 @@
         Log.i(TAG, getPrefix(obj) + msg);
     }
 
+    public static void wtf(Object obj, String msg) {
+        Log.wtf(TAG, getPrefix(obj) + msg);
+    }
+
     private static String getPrefix(Object obj) {
         return (obj == null ? "" : (obj.getClass().getSimpleName() + " - "));
     }