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
*/