Fix incoming callback and binding issues.
- Created queues to handle cases where callbacks were getting dropped while
the service was connecting.
- Fixed DeadObjectExceptions when the in-call ui was being re-installed.
- Change to a single connection object to fix connection leak where every
re-bind caused connection to double.
- Split caller information from Call into CallIdentification object.
Bug: 10468815
Bug: 10457750
Change-Id: Icaef130db2f6893df3175eed07d0d483a3c4c455
diff --git a/src/com/android/phone/BluetoothManager.java b/src/com/android/phone/BluetoothManager.java
index f5106c8..dd38632 100644
--- a/src/com/android/phone/BluetoothManager.java
+++ b/src/com/android/phone/BluetoothManager.java
@@ -408,14 +408,14 @@
}
@Override
- public void onIncoming(Call call, ArrayList<String> messages) {
+ public void onIncoming(Call call) {
// An incoming call can affect bluetooth indicator, so we update it whenever there is
// a change to any of the calls.
updateBluetoothIndication();
}
@Override
- public void onUpdate(List<Call> calls, boolean fullUpdate) {
+ public void onUpdate(List<Call> calls) {
updateBluetoothIndication();
}
diff --git a/src/com/android/phone/CallHandlerServiceProxy.java b/src/com/android/phone/CallHandlerServiceProxy.java
index 12e039e..75bb26c 100644
--- a/src/com/android/phone/CallHandlerServiceProxy.java
+++ b/src/com/android/phone/CallHandlerServiceProxy.java
@@ -20,7 +20,6 @@
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
-import android.os.AsyncResult;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
@@ -32,31 +31,51 @@
import com.android.services.telephony.common.AudioMode;
import com.android.services.telephony.common.Call;
import com.android.services.telephony.common.ICallHandlerService;
-import com.android.services.telephony.common.ICallCommandService;
+import com.google.common.collect.Lists;
-import java.util.ArrayList;
import java.util.List;
/**
* This class is responsible for passing through call state changes to the CallHandlerService.
*/
-public class CallHandlerServiceProxy extends Handler implements CallModeler.Listener,
- AudioModeListener {
+public class CallHandlerServiceProxy extends Handler
+ implements CallModeler.Listener, AudioModeListener {
private static final String TAG = CallHandlerServiceProxy.class.getSimpleName();
- private static final boolean DBG =
- (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
+ private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt(
+ "ro.debuggable", 0) == 1);
+ public static final int RETRY_DELAY_MILLIS = 2000;
+ private static final int BIND_RETRY_MSG = 1;
+ private static final int MAX_RETRY_COUNT = 5;
private AudioRouter mAudioRouter;
private CallCommandService mCallCommandService;
private CallModeler mCallModeler;
private Context mContext;
- private ICallHandlerService mCallHandlerService;
- private ServiceConnection mConnection;
+ private ICallHandlerService mCallHandlerServiceGuarded; // Guarded by mServiceAndQueueLock
+ private List<Call> mIncomingCallQueueGuarded; // Guarded by mServiceAndQueueLock
+ private List<List<Call>> mUpdateCallQueueGuarded; // Guarded by mServiceAndQueueLock
+ private List<Call> mDisconnectCallQueueGuarded; // Guarded by mServiceAndQueueLock
+ private final Object mServiceAndQueueLock = new Object();
+ private int mBindRetryCount = 0;
+
+ @Override
+ public void handleMessage(Message msg) {
+ super.handleMessage(msg);
+
+ switch (msg.what) {
+ case BIND_RETRY_MSG:
+ setupServiceConnection();
+ break;
+ }
+ }
public CallHandlerServiceProxy(Context context, CallModeler callModeler,
CallCommandService callCommandService, AudioRouter audioRouter) {
+ if (DBG) {
+ Log.d(TAG, "init CallHandlerServiceProxy");
+ }
mContext = context;
mCallCommandService = callCommandService;
mCallModeler = callModeler;
@@ -64,155 +83,263 @@
mAudioRouter.addAudioModeListener(this);
mCallModeler.addListener(this);
+
+ setupServiceConnection();
}
@Override
public void onDisconnect(Call call) {
- if (mCallHandlerService != null) {
- try {
- if (DBG) Log.d(TAG, "onDisconnect: " + call);
- mCallHandlerService.onDisconnect(call);
- maybeUnbind();
- } catch (RemoteException e) {
- Log.e(TAG, "Remote exception handling onDisconnect ", e);
+ try {
+ synchronized (mServiceAndQueueLock) {
+ if (mCallHandlerServiceGuarded == null) {
+ if (DBG) {
+ Log.d(TAG, "CallHandlerService not connected. Enqueue disconnect");
+ }
+ enqueueDisconnect(call);
+ return;
+ }
}
+ if (DBG) {
+ Log.d(TAG, "onDisconnect: " + call);
+ }
+ mCallHandlerServiceGuarded.onDisconnect(call);
+ } catch (Exception e) {
+ Log.e(TAG, "Remote exception handling onDisconnect ", e);
}
}
@Override
- public void onIncoming(Call call, ArrayList<String> textResponses) {
- if (maybeBindToService() && mCallHandlerService != null) {
- try {
- if (DBG) Log.d(TAG, "onIncoming: " + call);
- mCallHandlerService.onIncoming(call, textResponses);
- } catch (RemoteException e) {
- Log.e(TAG, "Remote exception handling onUpdate", e);
+ public void onIncoming(Call call) {
+ try {
+ synchronized (mServiceAndQueueLock) {
+ if (mCallHandlerServiceGuarded == null) {
+ if (DBG) {
+ Log.d(TAG, "CallHandlerService not connected. Enqueue incoming.");
+ }
+ enqueueIncoming(call);
+ return;
+ }
}
+ if (DBG) {
+ Log.d(TAG, "onIncoming: " + call);
+ }
+ // TODO(klp): check RespondViaSmsManager.allowRespondViaSmsForCall()
+ // must refactor call method to accept proper call object.
+ mCallHandlerServiceGuarded.onIncoming(call,
+ RejectWithTextMessageManager.loadCannedResponses());
+ } catch (Exception e) {
+ Log.e(TAG, "Remote exception handling onUpdate", e);
}
}
@Override
- public void onUpdate(List<Call> calls, boolean fullUpdate) {
- if (maybeBindToService() && mCallHandlerService != null) {
- try {
- if (DBG) Log.d(TAG, "onUpdate: " + calls.toString());
- mCallHandlerService.onUpdate(calls, fullUpdate);
- maybeUnbind();
- } catch (RemoteException e) {
- Log.e(TAG, "Remote exception handling onUpdate", e);
+ public void onUpdate(List<Call> calls) {
+ try {
+ synchronized (mServiceAndQueueLock) {
+ if (mCallHandlerServiceGuarded == null) {
+ if (DBG) {
+ Log.d(TAG, "CallHandlerService not connected. Enqueue update.");
+ }
+ enqueueUpdate(calls);
+ return;
+ }
}
+
+ if (DBG) {
+ Log.d(TAG, "onUpdate: " + calls.toString());
+ }
+ mCallHandlerServiceGuarded.onUpdate(calls);
+ } catch (Exception e) {
+ Log.e(TAG, "Remote exception handling onUpdate", e);
}
}
@Override
public void onAudioModeChange(int previousMode, int newMode) {
- // Just do a simple log for now.
- Log.i(TAG, "Updating with new audio mode: " + AudioMode.toString(newMode) +
- " from " + AudioMode.toString(previousMode));
-
- if (mCallHandlerService != null) {
- try {
- if (DBG) Log.d(TAG, "onSupportAudioModeChange");
-
- mCallHandlerService.onAudioModeChange(newMode);
- } catch (RemoteException e) {
- Log.e(TAG, "Remote exception handling onAudioModeChange", e);
+ try {
+ synchronized (mServiceAndQueueLock) {
+ // TODO(klp): does this need to be enqueued?
+ if (mCallHandlerServiceGuarded == null) {
+ if (DBG) {
+ Log.d(TAG, "CallHandlerService not conneccted. Skipping "
+ + "onAudioModeChange().");
+ }
+ return;
+ }
}
+
+ // Just do a simple log for now.
+ Log.i(TAG, "Updating with new audio mode: " + AudioMode.toString(newMode) +
+ " from " + AudioMode.toString(previousMode));
+
+ if (DBG) {
+ Log.d(TAG, "onSupportAudioModeChange");
+ }
+
+ mCallHandlerServiceGuarded.onAudioModeChange(newMode);
+ } catch (Exception e) {
+ Log.e(TAG, "Remote exception handling onAudioModeChange", e);
}
}
@Override
public void onSupportedAudioModeChange(int modeMask) {
- if (mCallHandlerService != null) {
- try {
- if (DBG) Log.d(TAG, "onSupportAudioModeChange: " + AudioMode.toString(modeMask));
+ try {
+ synchronized (mServiceAndQueueLock) {
+ // TODO(klp): does this need to be enqueued?
+ if (mCallHandlerServiceGuarded == null) {
+ if (DBG) {
+ Log.d(TAG, "CallHandlerService not conneccted. Skipping"
+ + "onSupportedAudioModeChange().");
+ }
+ return;
+ }
+ }
- mCallHandlerService.onSupportedAudioModeChange(modeMask);
- } catch (RemoteException e) {
- Log.e(TAG, "Remote exception handling onAudioModeChange", e);
+ if (DBG) {
+ Log.d(TAG, "onSupportAudioModeChange: " + AudioMode.toString(modeMask));
+ }
+
+ mCallHandlerServiceGuarded.onSupportedAudioModeChange(modeMask);
+ } catch (Exception e) {
+ Log.e(TAG, "Remote exception handling onAudioModeChange", e);
+ }
+
+ }
+
+ private ServiceConnection mConnection = new ServiceConnection() {
+ @Override public void onServiceConnected (ComponentName className, IBinder service){
+ if (DBG) {
+ Log.d(TAG, "Service Connected");
+ }
+ onCallHandlerServiceConnected(ICallHandlerService.Stub.asInterface(service));
+ }
+
+ @Override public void onServiceDisconnected (ComponentName className){
+ Log.i(TAG, "Disconnected from UI service.");
+ synchronized (mServiceAndQueueLock) {
+ mCallHandlerServiceGuarded = null;
+
+ // Technically, unbindService is un-necessary since the framework will schedule and
+ // restart the crashed service. But there is a exponential backoff for the restart.
+ // Unbind explicitly and setup again to avoid the backoff since it's important to
+ // always have an in call ui.
+ mContext.unbindService(mConnection);
+ setupServiceConnection();
}
}
}
+ ;
+
/**
* Sets up the connection with ICallHandlerService
*/
private void setupServiceConnection() {
- mConnection = new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName className, IBinder service) {
+ synchronized (mServiceAndQueueLock) {
+ if (mCallHandlerServiceGuarded == null) {
+
+
+ final Intent serviceIntent = new Intent(ICallHandlerService.class.getName());
+ final ComponentName component = new ComponentName(mContext.getResources().getString(
+ R.string.incall_ui_default_package), mContext.getResources().getString(
+ R.string.incall_ui_default_class));
+ serviceIntent.setComponent(component);
+
if (DBG) {
- Log.d(TAG, "Service Connected");
+ Log.d(TAG, "binding to service " + serviceIntent);
}
- onCallHandlerServiceConnected(ICallHandlerService.Stub.asInterface(service));
- }
-
- @Override
- public void onServiceDisconnected(ComponentName className) {
- Log.i(TAG, "Disconnected from UI service.");
- mCallHandlerService = null;
-
- // clean up our current binding.
- mContext.unbindService(mConnection);
- mConnection = null;
-
- // potentially attempt to rebind if there are still active calls.
- maybeBindToService();
- }
- };
-
- final Intent serviceIntent = new Intent(ICallHandlerService.class.getName());
- final ComponentName component = new ComponentName(
- mContext.getResources().getString(R.string.incall_ui_default_package),
- mContext.getResources().getString(R.string.incall_ui_default_class));
- serviceIntent.setComponent(component);
- if (!mContext.bindService(serviceIntent, mConnection, Context.BIND_AUTO_CREATE)) {
- Log.e(TAG, "Cound not bind to ICallHandlerService");
- }
- }
-
- /**
- * Checks To see if there are any calls left. If not, unbind the callhandler service.
- */
- private void maybeUnbind() {
- if (!mCallModeler.hasLiveCall()) {
- if (mConnection != null) {
- mContext.unbindService(mConnection);
- mConnection = null;
+ if (!mContext.bindService(serviceIntent, mConnection, Context.BIND_AUTO_CREATE)) {
+ // This happens when the in-call package is in the middle of being installed.
+ // Delay the retry.
+ mBindRetryCount++;
+ if (mBindRetryCount < MAX_RETRY_COUNT) {
+ Log.e(TAG, "bindService failed on " + serviceIntent + ". Retrying in " +
+ RETRY_DELAY_MILLIS + " ms.");
+ sendMessageDelayed(Message.obtain(this, BIND_RETRY_MSG),
+ RETRY_DELAY_MILLIS);
+ } else {
+ Log.wtf(TAG, "Tried to bind to in-call UI " + MAX_RETRY_COUNT + " times."
+ + " Giving up.");
+ }
+ }
}
}
}
/**
- * Checks to see if there are any active calls. If so, binds the call handler service.
- * @return true if already bound. False otherwise.
- */
- private boolean maybeBindToService() {
- if (mCallModeler.hasLiveCall()) {
- // mConnection is set to non-null once an attempt is made to connect.
- // We do not check against mCallHandlerService here because we could potentially
- // create multiple bindings to the UI.
- if (mConnection != null) {
- return true;
- }
- setupServiceConnection();
- }
- return false;
- }
-
- /**
* Called when the in-call UI service is connected. Send command interface to in-call.
*/
private void onCallHandlerServiceConnected(ICallHandlerService callHandlerService) {
- mCallHandlerService = callHandlerService;
+ synchronized (mServiceAndQueueLock) {
+ mCallHandlerServiceGuarded = callHandlerService;
+
+ // TODO(klp): combine queues into a single ordered queue.
+ processIncomingCallQueue();
+ processUpdateCallQueue();
+ processDisconnectQueue();
+ }
try {
- mCallHandlerService.setCallCommandService(mCallCommandService);
-
- // start with a full update
- onUpdate(mCallModeler.getFullList(), true);
+ mCallHandlerServiceGuarded.setCallCommandService(mCallCommandService);
} catch (RemoteException e) {
Log.e(TAG, "Remote exception calling CallHandlerService::setCallCommandService", e);
}
}
+
+
+ private void enqueueDisconnect(Call call) {
+ if (mDisconnectCallQueueGuarded == null) {
+ mDisconnectCallQueueGuarded = Lists.newArrayList();
+ }
+ mDisconnectCallQueueGuarded.add(new Call(call));
+ }
+
+ private void enqueueIncoming(Call call) {
+ if (mIncomingCallQueueGuarded == null) {
+ mIncomingCallQueueGuarded = Lists.newArrayList();
+ }
+ mIncomingCallQueueGuarded.add(new Call(call));
+ }
+
+ private void enqueueUpdate(List<Call> calls) {
+ if (mUpdateCallQueueGuarded == null) {
+ mUpdateCallQueueGuarded = Lists.newArrayList();
+ }
+ final List<Call> copy = Lists.newArrayList();
+ for (Call call : calls) {
+ copy.add(new Call(call));
+ }
+ mUpdateCallQueueGuarded.add(copy);
+ }
+
+ private void processDisconnectQueue() {
+ if (mDisconnectCallQueueGuarded != null) {
+ for (Call call : mDisconnectCallQueueGuarded) {
+ onDisconnect(call);
+ }
+ mDisconnectCallQueueGuarded.clear();
+ mDisconnectCallQueueGuarded = null;
+ }
+ }
+
+ private void processIncomingCallQueue() {
+ if (mIncomingCallQueueGuarded != null) {
+ for (Call call : mIncomingCallQueueGuarded) {
+ onIncoming(call);
+ }
+ mIncomingCallQueueGuarded.clear();
+ mIncomingCallQueueGuarded = null;
+ }
+ }
+
+ private void processUpdateCallQueue() {
+ if (mUpdateCallQueueGuarded != null) {
+ for (List<Call> calls : mUpdateCallQueueGuarded) {
+ onUpdate(calls);
+ }
+ mUpdateCallQueueGuarded.clear();
+ mUpdateCallQueueGuarded = null;
+ }
+ }
}
diff --git a/src/com/android/phone/CallModeler.java b/src/com/android/phone/CallModeler.java
index 94584a2..6b7a137 100644
--- a/src/com/android/phone/CallModeler.java
+++ b/src/com/android/phone/CallModeler.java
@@ -203,8 +203,7 @@
for (int i = 0; i < mListeners.size(); ++i) {
if (call != null) {
- mListeners.get(i).onIncoming(call,
- mRejectWithTextMessageManager.loadCannedResponses());
+ mListeners.get(i).onIncoming(call);
}
}
}
@@ -246,7 +245,7 @@
doUpdate(false, updatedCalls);
for (int i = 0; i < mListeners.size(); ++i) {
- mListeners.get(i).onUpdate(updatedCalls, false);
+ mListeners.get(i).onUpdate(updatedCalls);
}
}
@@ -680,8 +679,8 @@
*/
public interface Listener {
void onDisconnect(Call call);
- void onIncoming(Call call, ArrayList<String> textReponses);
- void onUpdate(List<Call> calls, boolean fullUpdate);
+ void onIncoming(Call call);
+ void onUpdate(List<Call> calls);
}
/**
diff --git a/src/com/android/phone/DTMFTonePlayer.java b/src/com/android/phone/DTMFTonePlayer.java
index 87c071a..d3ed53e 100644
--- a/src/com/android/phone/DTMFTonePlayer.java
+++ b/src/com/android/phone/DTMFTonePlayer.java
@@ -116,11 +116,11 @@
}
@Override
- public void onIncoming(Call call, ArrayList<String> textResponses) {
+ public void onIncoming(Call call) {
}
@Override
- public void onUpdate(List<Call> calls, boolean full) {
+ public void onUpdate(List<Call> calls) {
logD("Call updated");
checkCallState();
}
diff --git a/src/com/android/phone/RejectWithTextMessageManager.java b/src/com/android/phone/RejectWithTextMessageManager.java
index f032169..8324bd4 100644
--- a/src/com/android/phone/RejectWithTextMessageManager.java
+++ b/src/com/android/phone/RejectWithTextMessageManager.java
@@ -104,7 +104,7 @@
*
* @see com.android.phone.RejectWithTextMessageManager.Settings
*/
- public ArrayList<String> loadCannedResponses() {
+ public static ArrayList<String> loadCannedResponses() {
if (DBG) log("loadCannedResponses()...");
final SharedPreferences prefs = PhoneGlobals.getInstance().getSharedPreferences(