Merge "Add generic error code dialogs for failed calls." into klp-dev
diff --git a/InCallUI/src/com/android/incallui/AnswerPresenter.java b/InCallUI/src/com/android/incallui/AnswerPresenter.java
index e820929..b3deb64 100644
--- a/InCallUI/src/com/android/incallui/AnswerPresenter.java
+++ b/InCallUI/src/com/android/incallui/AnswerPresenter.java
@@ -64,6 +64,11 @@
     }
 
     @Override
+    public void onDisconnect(Call call) {
+        // 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.
diff --git a/InCallUI/src/com/android/incallui/CallList.java b/InCallUI/src/com/android/incallui/CallList.java
index 53288de..ba123b0 100644
--- a/InCallUI/src/com/android/incallui/CallList.java
+++ b/InCallUI/src/com/android/incallui/CallList.java
@@ -83,10 +83,15 @@
     public void onDisconnect(Call call) {
         Log.d(this, "onDisconnect: ", call);
 
-        updateCallInMap(call);
+        boolean updated = updateCallInMap(call);
 
-        notifyCallUpdateListeners(call);
-        notifyListenersOfChange();
+        if (updated) {
+            // notify those listening for changes on this specific change
+            notifyCallUpdateListeners(call);
+
+            // notify those listening for all disconnects
+            notifyListenersOfDisconnect(call);
+        }
     }
 
     /**
@@ -232,7 +237,6 @@
     }
 
     public Call getFirstCall() {
-        // TODO: should we switch to a simple list and pull the first one?
         Call result = getIncomingCall();
         if (result == null) {
             result = getOutgoingCall();
@@ -240,6 +244,12 @@
         if (result == null) {
             result = getFirstCallWithState(Call.State.ACTIVE);
         }
+        if (result == null) {
+            result = getDisconnectingCall();
+        }
+        if (result == null) {
+            result = getDisconnectedCall();
+        }
         return result;
     }
 
@@ -317,9 +327,21 @@
         }
     }
 
-    private void updateCallInMap(Call call) {
+    private void notifyListenersOfDisconnect(Call call) {
+        for (Listener listener : mListeners) {
+            listener.onDisconnect(call);
+        }
+    }
+
+    /**
+     * Updates the call entry in the local map.
+     * @return false if no call previously existed and no call was added, otherwise true.
+     */
+    private boolean updateCallInMap(Call call) {
         Preconditions.checkNotNull(call);
 
+        boolean updated = false;
+
         final Integer id = new Integer(call.getCallId());
 
         if (call.getState() == Call.State.DISCONNECTED) {
@@ -334,12 +356,17 @@
                 mHandler.sendMessageDelayed(msg, getDelayForDisconnect(call));
 
                 mCallMap.put(id, call);
+                updated = true;
             }
         } else if (!isCallDead(call)) {
             mCallMap.put(id, call);
+            updated = true;
         } else if (mCallMap.containsKey(id)) {
             mCallMap.remove(id);
+            updated = true;
         }
+
+        return updated;
     }
 
     private int getDelayForDisconnect(Call call) {
@@ -419,8 +446,28 @@
      * to the call list.
      */
     public interface Listener {
-        public void onCallListChange(CallList callList);
+        /**
+         * Called when a new incoming call comes in.
+         * This is the only method that gets called for incoming calls. Listeners
+         * that want to perform an action on incoming call should respond in this method
+         * because {@link #onCallListChange} does not automatically get called for
+         * incoming calls.
+         */
         public void onIncomingCall(Call call);
+
+        /**
+         * Called anytime there are changes to the call list.  The change can be switching call
+         * states, updating information, etc. This method will NOT be called for new incoming
+         * calls and for calls that switch to disconnected state. Listeners must add actions
+         * to those method implementations if they want to deal with those actions.
+         */
+        public void onCallListChange(CallList callList);
+
+        /**
+         * Called when a call switches to the disconnected state.  This is the only method
+         * that will get called upon disconnection.
+         */
+        public void onDisconnect(Call call);
     }
 
     public interface CallUpdateListener {
diff --git a/InCallUI/src/com/android/incallui/InCallActivity.java b/InCallUI/src/com/android/incallui/InCallActivity.java
index dfe9876..08766d2 100644
--- a/InCallUI/src/com/android/incallui/InCallActivity.java
+++ b/InCallUI/src/com/android/incallui/InCallActivity.java
@@ -20,6 +20,10 @@
 import com.android.services.telephony.common.Call.State;
 
 import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.DialogInterface.OnCancelListener;
 import android.content.Intent;
 import android.content.res.Configuration;
 import android.os.Bundle;
@@ -37,12 +41,15 @@
 
     public static final String SHOW_DIALPAD_EXTRA = "InCallActivity.show_dialpad";
 
+    private static final int INVALID_RES_ID = -1;
+
     private CallButtonFragment mCallButtonFragment;
     private CallCardFragment mCallCardFragment;
     private AnswerFragment mAnswerFragment;
     private DialpadFragment mDialpadFragment;
     private ConferenceManagerFragment mConferenceManagerFragment;
     private boolean mIsForegroundActivity;
+    private AlertDialog mDialog;
 
     /** Use to pass 'showDialpad' from {@link #onNewIntent} to {@link #onResume} */
     private boolean mShowDialpadRequested;
@@ -148,8 +155,12 @@
      */
     @Override
     public void finish() {
-        Log.d(this, "finish()...");
-        super.finish();
+        Log.i(this, "finish().  Dialog showing: " + (mDialog != null));
+
+        // skip finish if we are still showing a dialog.
+        if (mDialog == null) {
+            super.finish();
+        }
     }
 
     @Override
@@ -391,4 +402,72 @@
         }
         return super.dispatchPopulateAccessibilityEvent(event);
     }
+
+    public void maybeShowErrorDialogOnDisconnect(Call.DisconnectCause cause) {
+        Log.d(this, "maybeShowErrorDialogOnDisconnect");
+
+        if (!isFinishing()) {
+            final int resId = getResIdForDisconnectCause(cause);
+            if (resId != INVALID_RES_ID) {
+                showErrorDialog(resId);
+            }
+        }
+    }
+
+    public void dismissPendingDialogs() {
+        if (mDialog != null) {
+            mDialog.dismiss();
+            mDialog = null;
+        }
+    }
+
+    /**
+     * Utility function to bring up a generic "error" dialog.
+     */
+    private void showErrorDialog(int resId) {
+        final CharSequence msg = getResources().getText(resId);
+        Log.i(this, "Show Dialog: " + msg);
+
+        dismissPendingDialogs();
+
+        mDialog = new AlertDialog.Builder(this)
+            .setMessage(msg)
+            .setPositiveButton(R.string.ok, new OnClickListener() {
+                @Override
+                public void onClick(DialogInterface dialog, int which) {
+                    onDialogDismissed();
+                }})
+            .setOnCancelListener(new OnCancelListener() {
+                @Override
+                public void onCancel(DialogInterface dialog) {
+                    onDialogDismissed();
+                }})
+            .create();
+
+        mDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+        mDialog.show();
+    }
+
+    private int getResIdForDisconnectCause(Call.DisconnectCause cause) {
+        int resId = INVALID_RES_ID;
+
+        if (cause == Call.DisconnectCause.CALL_BARRED) {
+            resId = R.string.callFailed_cb_enabled;
+        } else if (cause == Call.DisconnectCause.FDN_BLOCKED) {
+            resId = R.string.callFailed_fdn_only;
+        } else if (cause == Call.DisconnectCause.CS_RESTRICTED) {
+            resId = R.string.callFailed_dsac_restricted;
+        } else if (cause == Call.DisconnectCause.CS_RESTRICTED_EMERGENCY) {
+            resId = R.string.callFailed_dsac_restricted_emergency;
+        } else if (cause == Call.DisconnectCause.CS_RESTRICTED_NORMAL) {
+            resId = R.string.callFailed_dsac_restricted_normal;
+        }
+
+        return resId;
+    }
+
+    private void onDialogDismissed() {
+        mDialog = null;
+        InCallPresenter.getInstance().onDismissDialog();
+    }
 }
diff --git a/InCallUI/src/com/android/incallui/InCallPresenter.java b/InCallUI/src/com/android/incallui/InCallPresenter.java
index 47fe911..02ac0be 100644
--- a/InCallUI/src/com/android/incallui/InCallPresenter.java
+++ b/InCallUI/src/com/android/incallui/InCallPresenter.java
@@ -119,6 +119,15 @@
         attemptCleanup();
     }
 
+    private void attemptFinishActivity() {
+        final boolean doFinish = (mInCallActivity != null && isActivityStarted());
+        Log.i(this, "Hide in call UI: " + doFinish);
+
+        if (doFinish) {
+            mInCallActivity.finish();
+        }
+    }
+
     /**
      * Called when the UI begins or ends. Starts the callstate callbacks if the UI just began.
      * Attempts to tear down everything if the UI just ended. See #tearDown for more insight on
@@ -128,16 +137,6 @@
         boolean updateListeners = false;
 
         if (inCallActivity != null) {
-            // When the UI comes up, we need to first check the state of the Service.
-            // If the service is not attached, that means that a call probably connected and
-            // then immediately disconnected before the UI was able to come up.  A disconnected
-            // service means we dont have calls, so start tearing down the UI instead.
-            if (mServiceConnected == false) {
-                inCallActivity.finish();
-                attemptCleanup();
-                return;
-            }
-
             if (mInCallActivity == null) {
                 updateListeners = true;
                 Log.i(this, "UI Initialized");
@@ -150,6 +149,24 @@
             }
 
             mInCallActivity = inCallActivity;
+
+            // By the time the UI finally comes up, the call may already be disconnected.
+            // If that's the case, we may need to show an error dialog.
+            if (mCallList != null && mCallList.getDisconnectedCall() != null) {
+                maybeShowErrorDialogOnDisconnect(mCallList.getDisconnectedCall());
+            }
+
+            // When the UI comes up, we need to first check the in-call state.
+            // If we are showing NO_CALLS, that means that a call probably connected and
+            // then immediately disconnected before the UI was able to come up.
+            // If we dont have any calls, start tearing down the UI instead.
+            // NOTE: This code relies on {@link #mInCallActivity} being set so we run it after
+            // it has been set.
+            if (mInCallState == InCallState.NO_CALLS) {
+                Log.i(this, "UI Intialized, but no calls left.  shut down.");
+                attemptFinishActivity();
+                return;
+            }
         } else {
             Log.i(this, "UI Destroyed)");
             updateListeners = true;
@@ -224,6 +241,18 @@
     }
 
     /**
+     * Called when a call becomes disconnected. Called everytime an existing call
+     * changes from being connected (incoming/outgoing/active) to disconnected.
+     */
+    @Override
+    public void onDisconnect(Call call) {
+        maybeShowErrorDialogOnDisconnect(call);
+
+        // We need to do the run the same code as onCallListChange.
+        onCallListChange(CallList.getInstance());
+    }
+
+    /**
      * Given the call list, return the state in which the in-call screen should be.
      */
     public static InCallState getPotentialStateFromCallList(CallList callList) {
@@ -359,6 +388,9 @@
         // "Swap calls", or can be a no-op, depending on the current state
         // of the Phone.
 
+        /**
+         * INCOMING CALL
+         */
         final CallList calls = CallList.getInstance();
         final Call incomingCall = calls.getIncomingCall();
         Log.v(this, "incomingCall: " + incomingCall);
@@ -369,8 +401,10 @@
             return true;
         }
 
+        /**
+         * ACTIVE CALL
+         */
         final Call activeCall = calls.getActiveCall();
-
         if (activeCall != null) {
             // TODO: This logic is repeated from CallButtonPresenter.java. We should
             // consolidate this logic.
@@ -399,8 +433,10 @@
             }
         }
 
+        /**
+         * BACKGROUND CALL
+         */
         final Call heldCall = calls.getBackgroundCall();
-
         if (heldCall != null) {
             // We have a hold call so presumeable it will always support HOLD...but
             // there is no harm in double checking.
@@ -420,6 +456,30 @@
     }
 
     /**
+     * A dialog could have prevented in-call screen from being previously finished.
+     * This function checks to see if there should be any UI left and if not attempts
+     * to tear down the UI.
+     */
+    public void onDismissDialog() {
+        Log.i(this, "Dialog dismissed");
+        if (mInCallState == InCallState.NO_CALLS) {
+            attemptFinishActivity();
+            attemptCleanup();
+        }
+    }
+
+    /**
+     * For some disconnected causes, we show a dialog.  This calls into the activity to show
+     * the dialog if appropriate for the call.
+     */
+    private void maybeShowErrorDialogOnDisconnect(Call call) {
+        // For newly disconnected calls, we may want to show a dialog on specific error conditions
+        if (isActivityStarted() && call.getState() == Call.State.DISCONNECTED) {
+            mInCallActivity.maybeShowErrorDialogOnDisconnect(call.getDisconnectCause());
+        }
+    }
+
+    /**
      * When the state of in-call changes, this is the first method to get called. It determines if
      * the UI needs to be started or finished depending on the new state and does it.
      */
@@ -486,16 +546,16 @@
             showInCall(false);
         } else if (startStartupSequence) {
             Log.i(this, "Start Full Screen in call UI");
+
+            // We're about the bring up the in-call UI for an incoming call. If we still have
+            // dialogs up, we need to clear them out before showing incoming screen.
+            if (isActivityStarted()) {
+                mInCallActivity.dismissPendingDialogs();
+            }
             mStatusBarNotifier.updateNotificationAndLaunchIncomingCallUi(newState, mCallList);
         } else if (newState == InCallState.NO_CALLS) {
-            Log.e(this, "Hide in call UI", new Exception());
-
             // The new state is the no calls state.  Tear everything down.
-            if (mInCallActivity != null) {
-                if (isActivityStarted()) {
-                    mInCallActivity.finish();
-                }
-            }
+            attemptFinishActivity();
         }
 
         return newState;
@@ -506,7 +566,8 @@
      * down.
      */
     private void attemptCleanup() {
-        boolean shouldCleanup = (mInCallActivity == null && !mServiceConnected);
+        boolean shouldCleanup = (mInCallActivity == null && !mServiceConnected &&
+                mInCallState == InCallState.NO_CALLS);
         Log.i(this, "attemptCleanup? " + shouldCleanup);
 
         if (shouldCleanup) {