Support connecting and disconnecting from Phone app.

Phone app now connects/disconnects from the UI when there are no active
calls but UI did not previously support being disconnected.  This change
adds the following functionality to support dis/reconnection:

- CallCommandClient now supports setting new ICallCommandService
  instances.
- CallCommandClient now checks if mCommandService is null before
  executing a method.
- CallHandlerService performs more careful ordered cleanup with
  destroyed including removing pending messages and clearing any
  existing calls from the call list.
- InCallActivity now performs tearDown of the presenters with onDestroy
  instead of finish() to reduce NPE instances.
- InCallActivity not notifies InCallPresenter when is finishes, and not
  just when it starts.
- InCallPresenter tears itself down after two things happen: the UI is
  destroyed and the service is disconnected.
- InCallPresenter now issues a full-screen restart of the UI if the
  UI was previously hidden (except for new outgoing calls). This allows
  the UI to come back to the foreground if it was in the foreground when
  the app went down.

The above changes also now protect against the phone app crashing or the
incall UI crashing.

bug: 10363682
Change-Id: I9b785f906f29015827e8e53e64cd5f5c72cd7981
diff --git a/InCallUI/src/com/android/incallui/CallButtonPresenter.java b/InCallUI/src/com/android/incallui/CallButtonPresenter.java
index 3c1ada1..12cc656 100644
--- a/InCallUI/src/com/android/incallui/CallButtonPresenter.java
+++ b/InCallUI/src/com/android/incallui/CallButtonPresenter.java
@@ -54,7 +54,6 @@
 
     @Override
     public void onStateChange(InCallState state, CallList callList) {
-
         if (state == InCallState.OUTGOING) {
             mCall = callList.getOutgoingCall();
         } else if (state == InCallState.INCALL) {
diff --git a/InCallUI/src/com/android/incallui/CallCommandClient.java b/InCallUI/src/com/android/incallui/CallCommandClient.java
index 280bab3..308776c 100644
--- a/InCallUI/src/com/android/incallui/CallCommandClient.java
+++ b/InCallUI/src/com/android/incallui/CallCommandClient.java
@@ -28,27 +28,27 @@
 
     private static CallCommandClient sInstance;
 
-    public static CallCommandClient getInstance() {
+    public static synchronized CallCommandClient getInstance() {
         if (sInstance == null) {
-            throw new IllegalStateException("CallCommandClient has not been initialized.");
+            sInstance = new CallCommandClient();
         }
         return sInstance;
     }
 
-    // TODO(klp): Not sure if static call is ok. Might need to switch to normal service binding.
-    public static void init(ICallCommandService service) {
-        Preconditions.checkState(sInstance == null);
-        sInstance = new CallCommandClient(service);
-    }
-
-
     private ICallCommandService mCommandService;
 
-    private CallCommandClient(ICallCommandService service) {
+    private CallCommandClient() {
+    }
+
+    public void setService(ICallCommandService service) {
         mCommandService = service;
     }
 
     public void answerCall(int callId) {
+        if (mCommandService == null) {
+            Logger.e(this, "Cannot answer call; CallCommandService == null");
+            return;
+        }
         try {
             mCommandService.answerCall(callId);
         } catch (RemoteException e) {
@@ -57,6 +57,10 @@
     }
 
     public void rejectCall(int callId, boolean rejectWithMessage, String message) {
+        if (mCommandService == null) {
+            Logger.e(this, "Cannot reject call; CallCommandService == null");
+            return;
+        }
         try {
             mCommandService.rejectCall(callId, rejectWithMessage, message);
         } catch (RemoteException e) {
@@ -65,6 +69,10 @@
     }
 
     public void disconnectCall(int callId) {
+        if (mCommandService == null) {
+            Logger.e(this, "Cannot disconnect call; CallCommandService == null");
+            return;
+        }
         try {
             mCommandService.disconnectCall(callId);
         } catch (RemoteException e) {
@@ -73,6 +81,10 @@
     }
 
     public void mute(boolean onOff) {
+        if (mCommandService == null) {
+            Logger.e(this, "Cannot mute call; CallCommandService == null");
+            return;
+        }
         try {
             mCommandService.mute(onOff);
         } catch (RemoteException e) {
@@ -81,6 +93,10 @@
     }
 
     public void hold(int callId, boolean onOff) {
+        if (mCommandService == null) {
+            Logger.e(this, "Cannot hold call; CallCommandService == null");
+            return;
+        }
         try {
             mCommandService.hold(callId, onOff);
         } catch (RemoteException e) {
@@ -89,6 +105,10 @@
     }
 
     public void merge() {
+        if (mCommandService == null) {
+            Logger.e(this, "Cannot merge call; CallCommandService == null");
+            return;
+        }
         try {
             mCommandService.merge();
         } catch (RemoteException e) {
@@ -97,6 +117,10 @@
     }
 
     public void swap() {
+        if (mCommandService == null) {
+            Logger.e(this, "Cannot swap call; CallCommandService == null");
+            return;
+        }
         try {
             mCommandService.swap();
         } catch (RemoteException e) {
@@ -105,6 +129,10 @@
     }
 
     public void addCall() {
+        if (mCommandService == null) {
+            Logger.e(this, "Cannot add call; CallCommandService == null");
+            return;
+        }
         try {
             mCommandService.addCall();
         } catch (RemoteException e) {
@@ -113,6 +141,10 @@
     }
 
     public void setAudioMode(int mode) {
+        if (mCommandService == null) {
+            Logger.e(this, "Cannot set audio mode; CallCommandService == null");
+            return;
+        }
         try {
             mCommandService.setAudioMode(mode);
         } catch (RemoteException e) {
@@ -121,6 +153,10 @@
     }
 
     public void playDtmfTone(char digit, boolean timedShortTone) {
+        if (mCommandService == null) {
+            Logger.e(this, "Cannot start dtmf tone; CallCommandService == null");
+            return;
+        }
         try {
             Logger.v(this, "Sending dtmf tone " + digit);
             mCommandService.playDtmfTone(digit, timedShortTone);
@@ -131,6 +167,10 @@
     }
 
     public void stopDtmfTone() {
+        if (mCommandService == null) {
+            Logger.e(this, "Cannot stop dtmf tone; CallCommandService == null");
+            return;
+        }
         try {
             Logger.v(this, "Stop dtmf tone ");
             mCommandService.stopDtmfTone();
diff --git a/InCallUI/src/com/android/incallui/CallHandlerService.java b/InCallUI/src/com/android/incallui/CallHandlerService.java
index bdee328..f0df1cd 100644
--- a/InCallUI/src/com/android/incallui/CallHandlerService.java
+++ b/InCallUI/src/com/android/incallui/CallHandlerService.java
@@ -44,6 +44,8 @@
     private static final int ON_SUPPORTED_AUDIO_MODE = 5;
     private static final int ON_DISCONNECT_CALL = 6;
 
+    private static final int LARGEST_MSG_ID = ON_DISCONNECT_CALL;
+
 
     private CallList mCallList;
     private Handler mMainHandler;
@@ -52,6 +54,7 @@
 
     @Override
     public void onCreate() {
+        Logger.d(this, "onCreate started");
         super.onCreate();
 
         mCallList = new CallList();
@@ -59,28 +62,52 @@
         mAudioModeProvider = new AudioModeProvider();
         mInCallPresenter = InCallPresenter.getInstance();
         mInCallPresenter.setUp(getApplicationContext(), mCallList, mAudioModeProvider);
+        Logger.d(this, "onCreate finished");
     }
 
     @Override
     public void onDestroy() {
+        Logger.d(this, "onDestroy started");
+
+        // Remove all pending messages before nulling out handler
+        for (int i = 1; i <= LARGEST_MSG_ID; i++) {
+            mMainHandler.removeMessages(i);
+        }
+        mMainHandler = null;
+
+        // The service gets disconnected under two circumstances:
+        // 1. When there are no more calls
+        // 2. When the phone app crashes.
+        // If (2) happens, we can't leave the UI thinking that there are still
+        // live calls.  So we will tell the callList to clear as a final request.
+        mCallList.clearOnDisconnect();
+        mCallList = null;
+
         mInCallPresenter.tearDown();
         mInCallPresenter = null;
         mAudioModeProvider = null;
-        mMainHandler = null;
-        mCallList = null;
+
+        Logger.d(this, "onDestroy finished");
     }
 
     @Override
     public IBinder onBind(Intent intent) {
+        Logger.d(this, "onBind");
         return mBinder;
     }
 
+    @Override
+    public boolean onUnbind(Intent intent) {
+        Logger.d(this, "onUnbind");
+        return true;
+    }
+
     private final ICallHandlerService.Stub mBinder = new ICallHandlerService.Stub() {
 
         @Override
         public void setCallCommandService(ICallCommandService service) {
             Logger.d(CallHandlerService.this, "onConnected: " + service.toString());
-            CallCommandClient.init(service);
+            CallCommandClient.getInstance().setService(service);
         }
 
         @Override
@@ -146,6 +173,12 @@
     }
 
     private void executeMessage(Message msg) {
+        if (msg.what > LARGEST_MSG_ID) {
+            // If you got here, you may have added a new message and forgotten to
+            // update LARGEST_MSG_ID
+            Logger.wtf(this, "Cannot handle message larger than LARGEST_MSG_ID.");
+        }
+
         Logger.d(this, "executeMessage " + msg.what);
 
         switch (msg.what) {
diff --git a/InCallUI/src/com/android/incallui/CallList.java b/InCallUI/src/com/android/incallui/CallList.java
index f692bf6..a3cee5f 100644
--- a/InCallUI/src/com/android/incallui/CallList.java
+++ b/InCallUI/src/com/android/incallui/CallList.java
@@ -225,6 +225,25 @@
     }
 
     /**
+     * This is called when the service disconnects, either expectedly or unexpectedly.
+     * For the expected case, it's because we have no calls left.  For the unexpected case,
+     * it is likely a crash of phone and we need to clean up our calls manually.  Without phone,
+     * there can be no active calls, so this is relatively safe thing to do.
+     */
+    public void clearOnDisconnect() {
+        for (Call call : mCallMap.values()) {
+            final int state = call.getState();
+            if (state != Call.State.IDLE &&
+                    state != Call.State.INVALID &&
+                    state != Call.State.DISCONNECTED) {
+                call.setState(Call.State.DISCONNECTED);
+                updateCallInMap(call);
+            }
+        }
+        notifyListenersOfChange();
+    }
+
+    /**
      * Sends a generic notification to all listeners that something has changed.
      * It is up to the listeners to call back to determine what changed.
      */
diff --git a/InCallUI/src/com/android/incallui/InCallActivity.java b/InCallUI/src/com/android/incallui/InCallActivity.java
index e1a9796..5edfaa3 100644
--- a/InCallUI/src/com/android/incallui/InCallActivity.java
+++ b/InCallUI/src/com/android/incallui/InCallActivity.java
@@ -88,6 +88,9 @@
     @Override
     protected void onDestroy() {
         Logger.d(this, "onDestroy()...  this = " + this);
+
+        tearDownPresenters();
+
         super.onDestroy();
     }
 
@@ -116,13 +119,7 @@
     @Override
     public void finish() {
         Logger.d(this, "finish()...");
-        tearDownPresenters();
-
         super.finish();
-
-        // TODO(klp): Actually finish the activity for now.  Revisit performance implications of
-        // this before launch.
-        // moveTaskToBack(true);
     }
 
     @Override
@@ -299,11 +296,14 @@
     }
 
     private void tearDownPresenters() {
+        Logger.d(this, "Tearing down presenters.");
         InCallPresenter mainPresenter = InCallPresenter.getInstance();
 
         mainPresenter.removeListener(mCallButtonFragment.getPresenter());
         mainPresenter.removeListener(mCallCardFragment.getPresenter());
         mainPresenter.removeListener(mAnswerFragment.getPresenter());
+
+        mainPresenter.setActivity(null);
     }
 
     private void toast(String text) {
diff --git a/InCallUI/src/com/android/incallui/InCallPresenter.java b/InCallUI/src/com/android/incallui/InCallPresenter.java
index d709ba4..3b400ef 100644
--- a/InCallUI/src/com/android/incallui/InCallPresenter.java
+++ b/InCallUI/src/com/android/incallui/InCallPresenter.java
@@ -47,7 +47,7 @@
     private Context mContext;
     private CallList mCallList;
     private InCallActivity mInCallActivity;
-
+    private boolean mServiceConnected = false;
     private InCallState mInCallState = InCallState.HIDDEN;
 
     public static synchronized InCallPresenter getInstance() {
@@ -71,34 +71,44 @@
 
         mAudioModeProvider = audioModeProvider;
 
+        // This only gets called by the service so this is okay.
+        mServiceConnected = true;
+
         Logger.d(this, "Finished InCallPresenter.setUp");
     }
 
+    /**
+     * Called when the telephony service has disconnected from us.  This will happen when there are
+     * no more active calls. However, we may still want to continue showing the UI for
+     * certain cases like showing "Call Ended".
+     * What we really want is to wait for the activity and the service to both disconnect before we
+     * tear things down. This method sets a serviceConnected boolean and calls a secondary method
+     * that performs the aforementioned logic.
+     */
     public void tearDown() {
-        mAudioModeProvider = null;
-
-        removeListener(mStatusBarNotifier);
-        mStatusBarNotifier = null;
-
-        mCallList.removeListener(this);
-        mCallList = null;
-
-        mContext = null;
-        mInCallActivity = null;
-
-        mListeners.clear();
-
-        Logger.d(this, "Finished InCallPresenter.tearDown");
+        Logger.d(this, "tearDown");
+        mServiceConnected = false;
+        attemptCleanup();
     }
 
+    /**
+     * 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
+     * the tear-down process.
+     */
     public void setActivity(InCallActivity inCallActivity) {
         mInCallActivity = inCallActivity;
 
-        Logger.d(this, "UI Initialized");
+        if (mInCallActivity != null) {
+            Logger.d(this, "UI Initialized");
 
-        // Since the UI just came up, imitate an update from the call list
-        // to set the proper UI state.
-        onCallListChange(mCallList);
+            // Since the UI just came up, imitate an update from the call list
+            // to set the proper UI state.
+            onCallListChange(mCallList);
+        } else {
+            Logger.d(this, "setActivity(null)");
+            attemptCleanup();
+        }
     }
 
     /**
@@ -187,7 +197,9 @@
     public void onUiShowing(boolean showing) {
         // We need to update the notification bar when we leave the UI because that
         // could trigger it to show again.
-        mStatusBarNotifier.updateNotification(mInCallState, mCallList);
+        if (mStatusBarNotifier != null) {
+            mStatusBarNotifier.updateNotification(mInCallState, mCallList);
+        }
     }
 
     /**
@@ -195,7 +207,7 @@
      * the UI needs to be started or finished depending on the new state and does it.
      */
     private InCallState startOrFinishUi(InCallState newState) {
-        Logger.d(this, "startOrFInishUi: " + newState.toString());
+        Logger.d(this, "startOrFinishUi: " + newState.toString());
 
         // TODO(klp): Consider a proper state machine implementation
 
@@ -228,8 +240,10 @@
         //          [ AND NOW YOU'RE IN THE CALL. voila! ]
         //
         // Our app is started using a fullScreen notification.  We need to do this whenever
-        // we get an incoming call.
-        final boolean startStartupSequence = (InCallState.INCOMING == newState);
+        // we get an incoming call or if this is the first time we are displaying (the previous
+        // state was HIDDEN).
+        final boolean startStartupSequence = (InCallState.INCOMING == newState ||
+                InCallState.HIDDEN == mInCallState);
 
         // A new outgoing call indicates that the user just now dialed a number and when that
         // happens we need to display the screen immediateley.
@@ -242,10 +256,10 @@
         Logger.v(this, "startStartupSequence: ", startStartupSequence);
 
 
-        if (startStartupSequence) {
-            mStatusBarNotifier.updateNotificationAndLaunchIncomingCallUi(newState, mCallList);
-        } else if (showCallUi) {
+        if (showCallUi) {
             showInCall();
+        } else if (startStartupSequence) {
+            mStatusBarNotifier.updateNotificationAndLaunchIncomingCallUi(newState, mCallList);
         } else if (newState == InCallState.HIDDEN) {
 
             // The new state is the hidden state (no calls).  Tear everything down.
@@ -265,6 +279,30 @@
         return newState;
     }
 
+    /**
+     * Checks to see if both the UI is gone and the service is disconnected. If so, tear it all
+     * down.
+     */
+    private void attemptCleanup() {
+        if (mInCallActivity == null && !mServiceConnected) {
+            Logger.d(this, "Start InCallPresenter.CleanUp");
+            mAudioModeProvider = null;
+
+            removeListener(mStatusBarNotifier);
+            mStatusBarNotifier = null;
+
+            mCallList.removeListener(this);
+            mCallList = null;
+
+            mContext = null;
+            mInCallActivity = null;
+
+            mListeners.clear();
+
+            Logger.d(this, "Finished InCallPresenter.CleanUp");
+        }
+    }
+
     private void showInCall() {
         Logger.d(this, "Showing in call manually.");
         mContext.startActivity(getInCallIntent());