Merge "Adding interfaces for phone number service." into klp-dev
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());