Merge "Handle providing disconnect message through CallRedirectionService." into sc-dev
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index c4f398c..0bdf58d 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -39,6 +39,7 @@
import android.provider.ContactsContract.Contacts;
import android.telecom.BluetoothCallQualityReport;
import android.telecom.CallAudioState;
+import android.telecom.CallDiagnosticService;
import android.telecom.CallerInfo;
import android.telecom.Conference;
import android.telecom.Connection;
@@ -60,6 +61,7 @@
import android.telephony.PhoneNumberUtils;
import android.telephony.TelephonyManager;
import android.telephony.emergency.EmergencyNumber;
+import android.telephony.ims.ImsReasonInfo;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.widget.Toast;
@@ -81,7 +83,9 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
/**
* Encapsulates all aspects of a given phone call throughout its lifecycle, starting
@@ -662,6 +666,22 @@
private boolean mIsSimCall;
/**
+ * Set to {@code true} if we received a valid response ({@code null} or otherwise) from
+ * the {@link DiagnosticCall#onCallDisconnected(ImsReasonInfo)} or
+ * {@link DiagnosticCall#onCallDisconnected(int, int)} calls. This is used to detect a timeout
+ * when awaiting a response from the call diagnostic service.
+ */
+ private boolean mReceivedCallDiagnosticPostCallResponse = false;
+
+ /**
+ * {@link CompletableFuture} used to delay posting disconnection and removal to a call until
+ * after a {@link CallDiagnosticService} is able to handle the disconnection and provide a
+ * disconnect message via {@link DiagnosticCall#onCallDisconnected(ImsReasonInfo)} or
+ * {@link DiagnosticCall#onCallDisconnected(int, int)}.
+ */
+ private CompletableFuture<Boolean> mDisconnectFuture;
+
+ /**
* Persists the specified parameters and initializes the new instance.
* @param context The context.
* @param repository The connection service repository.
@@ -1092,8 +1112,29 @@
}
}
+ /**
+ * Handles an incoming overridden disconnect message for this call.
+ *
+ * We only care if the disconnect is handled via a future.
+ * @param message the overridden disconnect message.
+ */
public void handleOverrideDisconnectMessage(@Nullable CharSequence message) {
+ Log.i(this, "handleOverrideDisconnectMessage; callid=%s, msg=%s", getId(), message);
+ if (isDisconnectHandledViaFuture()) {
+ mReceivedCallDiagnosticPostCallResponse = true;
+ if (message != null) {
+ Log.addEvent(this, LogUtils.Events.OVERRIDE_DISCONNECT_MESSAGE, message);
+ // Replace the existing disconnect cause in this call
+ setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.ERROR, message,
+ message, null));
+ }
+
+ mDisconnectFuture.complete(true);
+ } else {
+ Log.w(this, "handleOverrideDisconnectMessage; callid=%s - got override when unbound",
+ getId());
+ }
}
/**
@@ -4088,4 +4129,66 @@
public boolean isSimCall() {
return mIsSimCall;
}
+
+ /**
+ * Sets whether this is a sim call or not.
+ * @param isSimCall {@code true} if this is a SIM call, {@code false} otherwise.
+ */
+ public void setIsSimCall(boolean isSimCall) {
+ mIsSimCall = isSimCall;
+ }
+
+ /**
+ * Initializes a disconnect future which is used to chain up pending operations which take
+ * place when the {@link CallDiagnosticService} returns the result of the
+ * {@link DiagnosticCall#onCallDisconnected(int, int)} or
+ * {@link DiagnosticCall#onCallDisconnected(ImsReasonInfo)} invocation via
+ * {@link CallDiagnosticServiceAdapter}. If no {@link CallDiagnosticService} is in use, we
+ * would not try to make a disconnect future.
+ * @param timeoutMillis Timeout we use for waiting for the response.
+ * @return the {@link CompletableFuture}.
+ */
+ public CompletableFuture<Boolean> initializeDisconnectFuture(long timeoutMillis) {
+ if (mDisconnectFuture == null) {
+ mDisconnectFuture = new CompletableFuture<Boolean>()
+ .completeOnTimeout(false, timeoutMillis, TimeUnit.MILLISECONDS);
+ // After all the chained stuff we will report where the CDS timed out.
+ mDisconnectFuture.thenRunAsync(() -> {
+ if (!mReceivedCallDiagnosticPostCallResponse) {
+ Log.addEvent(this, LogUtils.Events.CALL_DIAGNOSTIC_SERVICE_TIMEOUT);
+ }},
+ new LoggedHandlerExecutor(mHandler, "C.iDF", mLock))
+ .exceptionally((throwable) -> {
+ Log.e(this, throwable, "Error while executing disconnect future");
+ return null;
+ });
+ }
+ return mDisconnectFuture;
+ }
+
+ /**
+ * @return the disconnect future, if initialized. Used for chaining operations after creation.
+ */
+ public CompletableFuture<Boolean> getDisconnectFuture() {
+ return mDisconnectFuture;
+ }
+
+ /**
+ * @return {@code true} if disconnection and removal is handled via a future, or {@code false}
+ * if this is handled immediately.
+ */
+ public boolean isDisconnectHandledViaFuture() {
+ return mDisconnectFuture != null && !mDisconnectFuture.isDone();
+ }
+
+ /**
+ * Perform any cleanup on this call as a result of a {@link TelecomServiceImpl}
+ * {@code cleanupStuckCalls} request.
+ */
+ public void cleanup() {
+ if (mDisconnectFuture != null) {
+ mDisconnectFuture.complete(false);
+ mDisconnectFuture = null;
+ }
+ }
}
diff --git a/src/com/android/server/telecom/CallDiagnosticServiceController.java b/src/com/android/server/telecom/CallDiagnosticServiceController.java
index 943a176..547bdcd 100644
--- a/src/com/android/server/telecom/CallDiagnosticServiceController.java
+++ b/src/com/android/server/telecom/CallDiagnosticServiceController.java
@@ -36,6 +36,7 @@
import android.telecom.CallDiagnosticService;
import android.telecom.ConnectionService;
import android.telecom.DiagnosticCall;
+import android.telecom.DisconnectCause;
import android.telecom.InCallService;
import android.telecom.Log;
import android.telecom.ParcelableCall;
@@ -255,6 +256,32 @@
}
/**
+ * Handles a newly disconnected call signalled from {@link CallsManager}.
+ * @param call The call
+ * @param disconnectCause The disconnect cause
+ * @return {@code true} if the {@link CallDiagnosticService} was sent the call, {@code false}
+ * if the call was not applicable to the CDS or if there was an issue sending it.
+ */
+ public boolean onCallDisconnected(@NonNull Call call,
+ @NonNull DisconnectCause disconnectCause) {
+ if (!call.isSimCall() || call.isExternalCall()) {
+ Log.i(this, "onCallDisconnected: skipping call %s as non-sim or external.",
+ call.getId());
+ return false;
+ }
+ String callId = mCallIdMapper.getCallId(call);
+ try {
+ if (isConnected()) {
+ mCallDiagnosticService.notifyCallDisconnected(callId, disconnectCause);
+ return true;
+ }
+ } catch (RemoteException e) {
+ Log.w(this, "onCallDisconnected: callId=%s, exception=%s", call.getId(), e);
+ }
+ return false;
+ }
+
+ /**
* Handles Telecom removal of calls; will remove the call from the bound service and if the
* number of tracked calls falls to zero, unbind from the service.
* @param call The call to remove from the bound CDS.
@@ -569,7 +596,7 @@
/**
* @return {@code true} if the call diagnostic service is bound/connected.
*/
- private boolean isConnected() {
+ public boolean isConnected() {
return mCallDiagnosticService != null;
}
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index d2d50ad..e6be6f6 100755
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -3099,27 +3099,82 @@
// be marked as missed.
call.setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.MISSED));
}
- call.setDisconnectCause(disconnectCause);
- setCallState(call, CallState.DISCONNECTED, "disconnected set explicitly");
- if(oldState == CallState.NEW && disconnectCause.getCode() == DisconnectCause.MISSED) {
+ // If a call diagnostic service is in use, we will log the original telephony-provided
+ // disconnect cause, inform the CDS of the disconnection, and then chain the update of the
+ // call state until AFTER the CDS reports it's result back.
+ if (oldState == CallState.ACTIVE && disconnectCause.getCode() != DisconnectCause.MISSED
+ && mCallDiagnosticServiceController.isConnected()
+ && mCallDiagnosticServiceController.onCallDisconnected(call, disconnectCause)) {
+ Log.i(this, "markCallAsDisconnected; callid=%s, postingToFuture.", call.getId());
+
+ // Log the original disconnect reason prior to calling into the
+ // CallDiagnosticService.
+ Log.addEvent(call, LogUtils.Events.SET_DISCONNECTED_ORIG, disconnectCause);
+
+ // Setup the future with a timeout so that the CDS is time boxed.
+ CompletableFuture<Boolean> future = call.initializeDisconnectFuture(
+ mTimeoutsAdapter.getCallDiagnosticServiceTimeoutMillis(
+ mContext.getContentResolver()));
+
+ // Post the disconnection updates to the future for completion once the CDS returns
+ // with it's overridden disconnect message.
+ future.thenRunAsync(() -> {
+ call.setDisconnectCause(disconnectCause);
+ setCallState(call, CallState.DISCONNECTED, "disconnected set explicitly");
+ }, new LoggedHandlerExecutor(mHandler, "CM.mCAD", mLock))
+ .exceptionally((throwable) -> {
+ Log.e(TAG, throwable, "Error while executing disconnect future.");
+ return null;
+ });
+ } else {
+ // No CallDiagnosticService, or it doesn't handle this call, so just do this
+ // synchronously as always.
+ call.setDisconnectCause(disconnectCause);
+ setCallState(call, CallState.DISCONNECTED, "disconnected set explicitly");
+ }
+
+ if (oldState == CallState.NEW && disconnectCause.getCode() == DisconnectCause.MISSED) {
Log.i(this, "markCallAsDisconnected: logging missed call ");
mCallLogManager.logCall(call, Calls.MISSED_TYPE, true, null);
}
-
}
/**
* Removes an existing disconnected call, and notifies the in-call app.
*/
void markCallAsRemoved(Call call) {
+ if (call.isDisconnectHandledViaFuture()) {
+ Log.i(this, "markCallAsRemoved; callid=%s, postingToFuture.", call.getId());
+ // A future is being used due to a CallDiagnosticService handling the call. We will
+ // chain the removal operation to the end of any outstanding disconnect work.
+ call.getDisconnectFuture().thenRunAsync(() -> {
+ performRemoval(call);
+ }, new LoggedHandlerExecutor(mHandler, "CM.mCAR", mLock))
+ .exceptionally((throwable) -> {
+ Log.e(TAG, throwable, "Error while executing disconnect future");
+ return null;
+ });
+
+ } else {
+ Log.i(this, "markCallAsRemoved; callid=%s, immediate.", call.getId());
+ performRemoval(call);
+ }
+ }
+
+ /**
+ * Work which is completed when a call is to be removed. Can either be be run synchronously or
+ * posted to a {@link Call#getDisconnectFuture()}.
+ * @param call The call.
+ */
+ private void performRemoval(Call call) {
mInCallController.getBindingFuture().thenRunAsync(() -> {
call.maybeCleanupHandover();
removeCall(call);
Call foregroundCall = mCallAudioManager.getPossiblyHeldForegroundCall();
if (mLocallyDisconnectingCalls.contains(call)) {
boolean isDisconnectingChildCall = call.isDisconnectingChildCall();
- Log.v(this, "markCallAsRemoved: isDisconnectingChildCall = "
+ Log.v(this, "performRemoval: isDisconnectingChildCall = "
+ isDisconnectingChildCall + "call -> %s", call);
mLocallyDisconnectingCalls.remove(call);
// Auto-unhold the foreground call due to a locally disconnected call, except if the
@@ -3136,10 +3191,11 @@
// The new foreground call is on hold, however the carrier does not display the hold
// button in the UI. Therefore, we need to auto unhold the held call since the user
// has no means of unholding it themselves.
- Log.i(this, "Auto-unholding held foreground call (call doesn't support hold)");
+ Log.i(this, "performRemoval: Auto-unholding held foreground call (call doesn't "
+ + "support hold)");
foregroundCall.unhold();
}
- }, new LoggedHandlerExecutor(mHandler, "CM.mCAR", mLock))
+ }, new LoggedHandlerExecutor(mHandler, "CM.pR", mLock))
.exceptionally((throwable) -> {
Log.e(TAG, throwable, "Error while executing call removal");
return null;
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 705fe4f..316a04d 100755
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -352,7 +352,7 @@
logIncoming("removeCall %s", callId);
Call call = mCallIdMapper.getCall(callId);
if (call != null) {
- if (call.isAlive()) {
+ if (call.isAlive() && !call.isDisconnectHandledViaFuture()) {
mCallsManager.markCallAsDisconnected(
call, new DisconnectCause(DisconnectCause.REMOTE));
} else {
diff --git a/src/com/android/server/telecom/LogUtils.java b/src/com/android/server/telecom/LogUtils.java
index a9bf18c..6270828 100644
--- a/src/com/android/server/telecom/LogUtils.java
+++ b/src/com/android/server/telecom/LogUtils.java
@@ -197,6 +197,10 @@
public static final String REDIRECTION_USER_CONFIRMED = "REDIRECTION_USER_CONFIRMED";
public static final String REDIRECTION_USER_CANCELLED = "REDIRECTION_USER_CANCELLED";
public static final String BT_QUALITY_REPORT = "BT_QUALITY_REPORT";
+ public static final String SET_DISCONNECTED_ORIG = "SET_DISCONNECTED_ORIG";
+ public static final String OVERRIDE_DISCONNECT_MESSAGE = "OVERRIDE_DISCONNECT_MSG";
+ public static final String CALL_DIAGNOSTIC_SERVICE_TIMEOUT =
+ "CALL_DIAGNOSTIC_SERVICE_TIMEOUT";
public static class Timings {
public static final String ACCEPT_TIMING = "accept";
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index a18ae6d..dd2d034 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -1776,6 +1776,8 @@
* {@link CallState#DISCONNECTED} or {@link CallState#DISCONNECTING} states. Stuck calls
* during CTS cause cascading failures, so if the CTS test detects such a state, it should
* call this method via a shell command to clean up before moving on to the next test.
+ * Also cleans up any pending futures related to
+ * {@link android.telecom.CallDiagnosticService}s.
*/
@Override
public void cleanupStuckCalls() {
@@ -1785,6 +1787,7 @@
enforceShellOnly(Binder.getCallingUid(), "cleanupStuckCalls");
Binder.withCleanCallingIdentity(() -> {
for (Call call : mCallsManager.getCalls()) {
+ call.cleanup();
if (call.getState() == CallState.DISCONNECTED
|| call.getState() == CallState.DISCONNECTING) {
mCallsManager.markCallAsRemoved(call);
diff --git a/src/com/android/server/telecom/Timeouts.java b/src/com/android/server/telecom/Timeouts.java
index 4d53d6d..22d1fbd 100644
--- a/src/com/android/server/telecom/Timeouts.java
+++ b/src/com/android/server/telecom/Timeouts.java
@@ -18,7 +18,11 @@
import android.content.ContentResolver;
import android.provider.Settings;
+import android.telecom.CallDiagnosticService;
import android.telecom.CallRedirectionService;
+import android.telecom.DiagnosticCall;
+import android.telephony.ims.ImsReasonInfo;
+
import java.util.concurrent.TimeUnit;
/**
@@ -67,6 +71,10 @@
public long getCallRecordingToneRepeatIntervalMillis(ContentResolver cr) {
return Timeouts.getCallRecordingToneRepeatIntervalMillis(cr);
}
+
+ public long getCallDiagnosticServiceTimeoutMillis(ContentResolver cr) {
+ return Timeouts.getCallDiagnosticServiceTimeoutMillis(cr);
+ }
}
/** A prefix to use for all keys so to not clobber the global namespace. */
@@ -190,7 +198,7 @@
/**
* Returns the amount of time for an user-defined {@link CallRedirectionService}.
*
- * @param contentResolver The content resolved.
+ * @param contentResolver The content resolver.
*/
public static long getUserDefinedCallRedirectionTimeoutMillis(ContentResolver contentResolver) {
return get(contentResolver, "user_defined_call_redirection_timeout",
@@ -200,7 +208,7 @@
/**
* Returns the amount of time for a carrier {@link CallRedirectionService}.
*
- * @param contentResolver The content resolved.
+ * @param contentResolver The content resolver.
*/
public static long getCarrierCallRedirectionTimeoutMillis(ContentResolver contentResolver) {
return get(contentResolver, "carrier_call_redirection_timeout", 5000L /* 5 seconds */);
@@ -214,6 +222,17 @@
}
/**
+ * Returns the maximum amount of time a {@link CallDiagnosticService} is permitted to take to
+ * return back from {@link DiagnosticCall#onCallDisconnected(ImsReasonInfo)} and
+ * {@link DiagnosticCall#onCallDisconnected(int, int)}.
+ * @param contentResolver The resolver for the config option.
+ * @return The timeout in millis.
+ */
+ public static long getCallDiagnosticServiceTimeoutMillis(ContentResolver contentResolver) {
+ return get(contentResolver, "call_diagnostic_service_timeout", 2000L /* 2 sec */);
+ }
+
+ /**
* Returns the number of milliseconds for which the system should exempt the default dialer from
* power save restrictions due to the dialer needing to handle a missed call notification
* (update call log, check VVM, etc...).
diff --git a/testapps/src/com/android/server/telecom/testapps/TestCallDiagnosticService.java b/testapps/src/com/android/server/telecom/testapps/TestCallDiagnosticService.java
index 73bf438..36554ec 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestCallDiagnosticService.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestCallDiagnosticService.java
@@ -51,14 +51,14 @@
@Override
public CharSequence onCallDisconnected(int disconnectCause, int preciseDisconnectCause) {
Log.i(this, "onCallDisconnected");
- return null;
+ return "GSM/CDMA call dropped because " + disconnectCause;
}
@Nullable
@Override
public CharSequence onCallDisconnected(@NonNull ImsReasonInfo disconnectReason) {
Log.i(this, "onCallDisconnected");
- return null;
+ return "ImsCall dropped because something happened " + disconnectReason.mExtraMessage;
}
@Override
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index 08f3536..71e6b35 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -41,6 +41,7 @@
import static org.mockito.Mockito.when;
import android.content.ComponentName;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
@@ -52,6 +53,7 @@
import android.os.UserHandle;
import android.telecom.CallerInfo;
import android.telecom.Connection;
+import android.telecom.DisconnectCause;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
@@ -228,6 +230,8 @@
doNothing().when(mRoleManagerAdapter).setCurrentUserHandle(any());
when(mDisconnectedCallNotifierFactory.create(any(Context.class),any(CallsManager.class)))
.thenReturn(mDisconnectedCallNotifier);
+ when(mTimeoutsAdapter.getCallDiagnosticServiceTimeoutMillis(any(ContentResolver.class)))
+ .thenReturn(2000L);
mCallsManager = new CallsManager(
mComponentContextFixture.getTestDouble().getApplicationContext(),
mLock,
@@ -1508,6 +1512,39 @@
eq(CallState.ACTIVE));
}
+ /**
+ * Verifies where a call diagnostic service is NOT in use that we don't try to relay to the
+ * CallDiagnosticService and that we get a synchronous disconnect.
+ * @throws Exception
+ */
+ @MediumTest
+ @Test
+ public void testDisconnectCallSynchronous() throws Exception {
+ Call callSpy = addSpyCall();
+ callSpy.setIsSimCall(true);
+ when(mCallDiagnosticServiceController.isConnected()).thenReturn(false);
+ mCallsManager.markCallAsDisconnected(callSpy, new DisconnectCause(DisconnectCause.ERROR));
+
+ verify(mCallDiagnosticServiceController, never()).onCallDisconnected(any(Call.class),
+ any(DisconnectCause.class));
+ verify(callSpy).setDisconnectCause(any(DisconnectCause.class));
+ }
+
+ @MediumTest
+ @Test
+ public void testDisconnectCallAsynchronous() throws Exception {
+ Call callSpy = addSpyCall();
+ callSpy.setIsSimCall(true);
+ when(mCallDiagnosticServiceController.isConnected()).thenReturn(true);
+ when(mCallDiagnosticServiceController.onCallDisconnected(any(Call.class),
+ any(DisconnectCause.class))).thenReturn(true);
+ mCallsManager.markCallAsDisconnected(callSpy, new DisconnectCause(DisconnectCause.ERROR));
+
+ verify(mCallDiagnosticServiceController).onCallDisconnected(any(Call.class),
+ any(DisconnectCause.class));
+ verify(callSpy, never()).setDisconnectCause(any(DisconnectCause.class));
+ }
+
private Call addSpyCall() {
return addSpyCall(SIM_2_HANDLE, CallState.ACTIVE);
}