Added some APIs to TelephonyManager

toggleHold()
merge()
mute(boolean mute)
playDtmfTone(char digit, boolean timedShortCode)
stopDtmfTone()
swap()
addListener(ITelephonyListener listener)

Also add ITelephonyListener to get more detailed phone updates

Bug: 13302451
Change-Id: Iaf5970700bb107d2735a97a66cbb82477355bfe4
diff --git a/src/com/android/phone/CallCommandService.java b/src/com/android/phone/CallCommandService.java
index 5238911..2076979 100644
--- a/src/com/android/phone/CallCommandService.java
+++ b/src/com/android/phone/CallCommandService.java
@@ -168,30 +168,10 @@
 
     @Override
     public void swap() {
-        if (!PhoneUtils.okToSwapCalls(mCallManager)) {
-            // TODO: throw an error instead?
-            return;
-        }
-
-        // Swap the fg and bg calls.
-        // In the future we may provides some way for user to choose among
-        // multiple background calls, for now, always act on the first background calll.
-        PhoneUtils.switchHoldingAndActive(mCallManager.getFirstActiveBgCall());
-
-        final PhoneGlobals mApp = PhoneGlobals.getInstance();
-
-        // If we have a valid BluetoothPhoneService then since CDMA network or
-        // Telephony FW does not send us information on which caller got swapped
-        // we need to update the second call active state in BluetoothPhoneService internally
-        if (mCallManager.getBgPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
-            final IBluetoothHeadsetPhone btPhone = mApp.getBluetoothPhoneService();
-            if (btPhone != null) {
-                try {
-                    btPhone.cdmaSwapSecondCallState();
-                } catch (RemoteException e) {
-                    Log.e(TAG, Log.getStackTraceString(new Throwable()));
-                }
-            }
+        try {
+            PhoneUtils.swap();
+        } catch (Exception e) {
+            Log.e(TAG, "Error during swap().", e);
         }
     }
 
diff --git a/src/com/android/phone/PhoneGlobals.java b/src/com/android/phone/PhoneGlobals.java
index 1ce99a3..62d542f 100644
--- a/src/com/android/phone/PhoneGlobals.java
+++ b/src/com/android/phone/PhoneGlobals.java
@@ -489,7 +489,8 @@
             callHandlerServiceProxy = new CallHandlerServiceProxy(this, callModeler,
                     callCommandService, audioRouter);
 
-            phoneMgr = PhoneInterfaceManager.init(this, phone, callHandlerServiceProxy);
+            phoneMgr = PhoneInterfaceManager.init(this, phone, callHandlerServiceProxy, callModeler,
+                    dtmfTonePlayer);
 
             // Create the CallNotifer singleton, which handles
             // asynchronous events from the telephony layer (like
diff --git a/src/com/android/phone/PhoneInterfaceManager.java b/src/com/android/phone/PhoneInterfaceManager.java
index 82b5e9f..cf86e7a 100644
--- a/src/com/android/phone/PhoneInterfaceManager.java
+++ b/src/com/android/phone/PhoneInterfaceManager.java
@@ -18,6 +18,7 @@
 
 import android.app.ActivityManager;
 import android.app.AppOpsManager;
+import android.bluetooth.IBluetoothHeadsetPhone;
 import android.content.ActivityNotFoundException;
 import android.content.Context;
 import android.content.Intent;
@@ -27,9 +28,11 @@
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
 import android.os.Process;
+import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.UserHandle;
 import android.telephony.NeighboringCellInfo;
@@ -38,21 +41,29 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import com.android.internal.telephony.CallManager;
+import com.android.internal.telephony.CommandException;
+import com.android.internal.telephony.Connection;
 import com.android.internal.telephony.DefaultPhoneNotifier;
 import com.android.internal.telephony.IccCard;
 import com.android.internal.telephony.ITelephony;
+import com.android.internal.telephony.ITelephonyListener;
 import com.android.internal.telephony.Phone;
-import com.android.internal.telephony.CallManager;
-import com.android.internal.telephony.CommandException;
 import com.android.internal.telephony.PhoneConstants;
+import com.android.services.telephony.common.Call;
 
-import java.util.List;
+import com.android.internal.util.HexDump;
+
 import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
 
 /**
  * Implementation of the ITelephony interface.
  */
-public class PhoneInterfaceManager extends ITelephony.Stub {
+public class PhoneInterfaceManager extends ITelephony.Stub implements CallModeler.Listener {
     private static final String LOG_TAG = "PhoneInterfaceManager";
     private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
     private static final boolean DBG_LOC = false;
@@ -74,6 +85,14 @@
     AppOpsManager mAppOps;
     MainThreadHandler mMainThreadHandler;
     CallHandlerServiceProxy mCallHandlerService;
+    CallModeler mCallModeler;
+    DTMFTonePlayer mDtmfTonePlayer;
+    Handler mDtmfStopHandler = new Handler();
+    Runnable mDtmfStopRunnable;
+
+    private final List<ITelephonyListener> mListeners = new ArrayList<ITelephonyListener>();
+    private final Map<IBinder, TelephonyListenerDeathRecipient> mDeathRecipients =
+            new HashMap<IBinder, TelephonyListenerDeathRecipient>();
 
     /**
      * A request object for use with {@link MainThreadHandler}. Requesters should wait() on the
@@ -221,10 +240,12 @@
      * This is only done once, at startup, from PhoneApp.onCreate().
      */
     /* package */ static PhoneInterfaceManager init(PhoneGlobals app, Phone phone,
-            CallHandlerServiceProxy callHandlerService) {
+                CallHandlerServiceProxy callHandlerService, CallModeler callModeler,
+                DTMFTonePlayer dtmfTonePlayer) {
         synchronized (PhoneInterfaceManager.class) {
             if (sInstance == null) {
-                sInstance = new PhoneInterfaceManager(app, phone, callHandlerService);
+                sInstance = new PhoneInterfaceManager(app, phone, callHandlerService, callModeler,
+                        dtmfTonePlayer);
             } else {
                 Log.wtf(LOG_TAG, "init() called multiple times!  sInstance = " + sInstance);
             }
@@ -234,13 +255,17 @@
 
     /** Private constructor; @see init() */
     private PhoneInterfaceManager(PhoneGlobals app, Phone phone,
-            CallHandlerServiceProxy callHandlerService) {
+            CallHandlerServiceProxy callHandlerService, CallModeler callModeler,
+            DTMFTonePlayer dtmfTonePlayer) {
         mApp = app;
         mPhone = phone;
         mCM = PhoneGlobals.getInstance().mCM;
         mAppOps = (AppOpsManager)app.getSystemService(Context.APP_OPS_SERVICE);
         mMainThreadHandler = new MainThreadHandler();
         mCallHandlerService = callHandlerService;
+        mCallModeler = callModeler;
+        mCallModeler.addListener(this);
+        mDtmfTonePlayer = dtmfTonePlayer;
         publish();
     }
 
@@ -793,6 +818,15 @@
         mApp.enforceCallingOrSelfPermission(android.Manifest.permission.CALL_PHONE, null);
     }
 
+    /**
+     * Make sure the caller has the READ_PRIVILEGED_PHONE_STATE permission.
+     *
+     * @throws SecurityException if the caller does not have the required permission
+     */
+    private void enforcePrivilegedPhoneStatePermission() {
+        mApp.enforceCallingOrSelfPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+                null);
+    }
 
     private String createTelUrl(String number) {
         if (TextUtils.isEmpty(number)) {
@@ -897,4 +931,248 @@
     public int getLteOnCdmaMode() {
         return mPhone.getLteOnCdmaMode();
     }
+
+    @Override
+    public void toggleHold() {
+        enforceModifyPermission();
+
+        try {
+            PhoneUtils.switchHoldingAndActive(mCM.getFirstActiveBgCall());
+        } catch (Exception e) {
+            Log.e(LOG_TAG, "Error during toggleHold().", e);
+        }
+    }
+
+    @Override
+    public void merge() {
+        enforceModifyPermission();
+
+        try {
+            if (PhoneUtils.okToMergeCalls(mCM)) {
+                PhoneUtils.mergeCalls(mCM);
+            }
+        } catch (Exception e) {
+            Log.e(LOG_TAG, "Error during merge().", e);
+        }
+    }
+
+    @Override
+    public void swap() {
+        enforceModifyPermission();
+
+        try {
+            PhoneUtils.swap();
+        } catch (Exception e) {
+            Log.e(LOG_TAG, "Error during swap().", e);
+        }
+    }
+
+    @Override
+    public void mute(boolean onOff) {
+        enforceModifyPermission();
+
+        try {
+            PhoneUtils.setMute(onOff);
+        } catch (Exception e) {
+            Log.e(LOG_TAG, "Error during mute().", e);
+        }
+    }
+
+    @Override
+    public void playDtmfTone(char digit, boolean timedShortTone) {
+        enforceModifyPermission();
+
+        synchronized (mDtmfStopHandler) {
+            try {
+                mDtmfTonePlayer.playDtmfTone(digit, timedShortTone);
+            } catch (Exception e) {
+                Log.e(LOG_TAG, "Error playing DTMF tone.", e);
+            }
+
+            if (mDtmfStopRunnable != null) {
+                mDtmfStopHandler.removeCallbacks(mDtmfStopRunnable);
+            }
+            mDtmfStopRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    synchronized (mDtmfStopHandler) {
+                        if (mDtmfStopRunnable == this) {
+                            mDtmfTonePlayer.stopDtmfTone();
+                            mDtmfStopRunnable = null;
+                        }
+                    }
+                }
+            };
+            mDtmfStopHandler.postDelayed(mDtmfStopRunnable, 5000);
+        }
+    }
+
+    @Override
+    public void stopDtmfTone() {
+        enforceModifyPermission();
+
+        synchronized (mDtmfStopHandler) {
+            try {
+                mDtmfTonePlayer.stopDtmfTone();
+            } catch (Exception e) {
+                Log.e(LOG_TAG, "Error stopping DTMF tone.", e);
+            }
+
+            if (mDtmfStopRunnable != null) {
+                mDtmfStopHandler.removeCallbacks(mDtmfStopRunnable);
+                mDtmfStopRunnable = null;
+            }
+        }
+    }
+
+    @Override
+    public void addListener(ITelephonyListener listener) {
+        enforcePrivilegedPhoneStatePermission();
+
+        if (listener == null) {
+            throw new IllegalArgumentException("Listener must not be null.");
+        }
+
+        synchronized (mListeners) {
+            IBinder listenerBinder = listener.asBinder();
+            for (ITelephonyListener l : mListeners) {
+                if (l.asBinder().equals(listenerBinder)) {
+                    Log.w(LOG_TAG, "Listener already registered. Ignoring.");
+                    return;
+                }
+            }
+            mListeners.add(listener);
+            mDeathRecipients.put(listener.asBinder(),
+                    new TelephonyListenerDeathRecipient(listener.asBinder()));
+
+            // update the new listener so they get the full call state immediately
+            for (Call call : mCallModeler.getFullList()) {
+                try {
+                    notifyListenerOfCallLocked(call, listener);
+                } catch (RemoteException e) {
+                    Log.e(LOG_TAG, "Error updating new listener. Ignoring.");
+                    removeListenerInternal(listener);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void removeListener(ITelephonyListener listener) {
+        enforcePrivilegedPhoneStatePermission();
+
+        if (listener == null) {
+            throw new IllegalArgumentException("Listener must not be null.");
+        }
+
+        removeListenerInternal(listener);
+    }
+
+    private void removeListenerInternal(ITelephonyListener listener) {
+        IBinder listenerBinder = listener.asBinder();
+
+        synchronized (mListeners) {
+            for (Iterator<ITelephonyListener> it = mListeners.iterator(); it.hasNext(); ) {
+                ITelephonyListener nextListener = it.next();
+                if (nextListener.asBinder().equals(listenerBinder)) {
+                    TelephonyListenerDeathRecipient dr = mDeathRecipients.get(listener.asBinder());
+                    if (dr != null) {
+                        dr.unlinkDeathRecipient();
+                    }
+                    it.remove();
+                }
+            }
+        }
+    }
+
+    /** CallModeler.Listener implementation **/
+
+    @Override
+    public void onDisconnect(Call call) {
+        notifyListenersOfCall(call);
+    }
+
+    @Override
+    public void onIncoming(Call call) {
+        notifyListenersOfCall(call);
+    }
+
+    @Override
+    public void onUpdate(List<Call> calls) {
+        for (Call call : calls) {
+            notifyListenersOfCall(call);
+        }
+    }
+
+    @Override
+    public void onPostDialAction(
+            Connection.PostDialState state, int callId, String remainingChars, char c) { }
+
+    private void notifyListenersOfCall(Call call) {
+        synchronized (mListeners) {
+            for (Iterator<ITelephonyListener> it = mListeners.iterator(); it.hasNext(); ) {
+                ITelephonyListener listener = it.next();
+                try {
+                    notifyListenerOfCallLocked(call, listener);
+                } catch (RemoteException e) {
+                    TelephonyListenerDeathRecipient deathRecipient =
+                            mDeathRecipients.get(listener.asBinder());
+                    if (deathRecipient != null) {
+                        deathRecipient.unlinkDeathRecipient();
+                    }
+                    it.remove();
+                }
+            }
+        }
+    }
+
+    private void notifyListenerOfCallLocked(final Call call,final ITelephonyListener listener)
+            throws RemoteException {
+        if (Binder.isProxy(listener)) {
+            listener.onUpdate(call.getCallId(), call.getState(), call.getNumber());
+        } else {
+            mMainThreadHandler.post(new Runnable() {
+
+                @Override
+                public void run() {
+                    try {
+                        listener.onUpdate(call.getCallId(), call.getState(), call.getNumber());
+                    } catch (RemoteException e) {
+                        Log.wtf(LOG_TAG, "Local binder call failed with RemoteException.", e);
+                    }
+                }
+            });
+        }
+
+    }
+
+    private class TelephonyListenerDeathRecipient implements Binder.DeathRecipient {
+        private final IBinder mBinder;
+
+        public TelephonyListenerDeathRecipient(IBinder listener) {
+            mBinder = listener;
+            try {
+                mBinder.linkToDeath(this, 0);
+            } catch (RemoteException e) {
+                unlinkDeathRecipient();
+            }
+        }
+
+        @Override
+        public void binderDied() {
+            synchronized (mListeners) {
+                if (mListeners.contains(mBinder)) {
+                    mListeners.remove(mBinder);
+                    Log.w(LOG_TAG, "ITelephonyListener died. Removing.");
+                } else {
+                    Log.w(LOG_TAG, "TelephonyListener binder died but the listener " +
+                            "is not registered.");
+                }
+            }
+        }
+
+        public void unlinkDeathRecipient() {
+            mBinder.unlinkToDeath(this, 0);
+        }
+    }
 }
diff --git a/src/com/android/phone/PhoneUtils.java b/src/com/android/phone/PhoneUtils.java
index 079f96e..e03171e 100644
--- a/src/com/android/phone/PhoneUtils.java
+++ b/src/com/android/phone/PhoneUtils.java
@@ -837,6 +837,33 @@
         }
     }
 
+    static void swap() {
+        final PhoneGlobals mApp = PhoneGlobals.getInstance();
+        if (!okToSwapCalls(mApp.mCM)) {
+            // TODO: throw an error instead?
+            return;
+        }
+
+        // Swap the fg and bg calls.
+        // In the future we may provide some way for user to choose among
+        // multiple background calls, for now, always act on the first background call.
+        PhoneUtils.switchHoldingAndActive(mApp.mCM.getFirstActiveBgCall());
+
+        // If we have a valid BluetoothPhoneService then since CDMA network or
+        // Telephony FW does not send us information on which caller got swapped
+        // we need to update the second call active state in BluetoothPhoneService internally
+        if (mApp.mCM.getBgPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
+            final IBluetoothHeadsetPhone btPhone = mApp.getBluetoothPhoneService();
+            if (btPhone != null) {
+                try {
+                    btPhone.cdmaSwapSecondCallState();
+                } catch (RemoteException e) {
+                    Log.e(LOG_TAG, Log.getStackTraceString(new Throwable()));
+                }
+            }
+        }
+    }
+
     /**
      * @param heldCall is the background call want to be swapped
      */