Merge "Support TOGGLE_MUTE in CallAudioRouteController" into main
diff --git a/flags/telecom_anomaly_report_flags.aconfig b/flags/telecom_anomaly_report_flags.aconfig
index b060ed0..5d42b86 100644
--- a/flags/telecom_anomaly_report_flags.aconfig
+++ b/flags/telecom_anomaly_report_flags.aconfig
@@ -16,3 +16,14 @@
description: "If a self-managed call is stuck in certain states, disconnect it"
bug: "360298368"
}
+
+# OWNER=tgunn TARGET=25Q2
+flag {
+ name: "dont_timeout_destroyed_calls"
+ namespace: "telecom"
+ description: "When create connection timeout is hit, if call is already destroyed, skip anomaly"
+ bug: "381684580"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/flags/telecom_api_flags.aconfig b/flags/telecom_api_flags.aconfig
index 75efdfa..2dfd878 100644
--- a/flags/telecom_api_flags.aconfig
+++ b/flags/telecom_api_flags.aconfig
@@ -73,3 +73,12 @@
description: "Formalizes the getLastKnownCellIdentity API that Telecom reliees on as a system api"
bug: "327454165"
}
+
+# OWNER=grantmenke TARGET=25Q2
+flag {
+ name: "allow_system_apps_resolve_voip_calls"
+ is_exported: true
+ namespace: "telecom"
+ description: "Allow system apps such as accessibility to accept and end VOIP calls."
+ bug: "353579043"
+}
diff --git a/flags/telecom_bluetoothdevicemanager_flags.aconfig b/flags/telecom_bluetoothdevicemanager_flags.aconfig
index 4c91491..1c8bd0c 100644
--- a/flags/telecom_bluetoothdevicemanager_flags.aconfig
+++ b/flags/telecom_bluetoothdevicemanager_flags.aconfig
@@ -8,3 +8,23 @@
description: "Fix for Log.wtf in the BinderProxy"
bug: "333417369"
}
+# OWNER=huiwang TARGET=25Q1
+flag {
+ name: "keep_bluetooth_devices_cache_updated"
+ namespace: "telecom"
+ description: "Fix the devices cache issue of BluetoothDeviceManager"
+ bug: "380320985"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+# OWNER=grantmenke TARGET=25Q2
+flag {
+ name: "skip_baseline_switch_when_route_not_bluetooth"
+ namespace: "telecom"
+ description: "Only switch back to baseline if the call audio is currently routed to bluetooth"
+ bug: "333417369"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
\ No newline at end of file
diff --git a/flags/telecom_callaudioroutestatemachine_flags.aconfig b/flags/telecom_callaudioroutestatemachine_flags.aconfig
index a60c0f1..e86db31 100644
--- a/flags/telecom_callaudioroutestatemachine_flags.aconfig
+++ b/flags/telecom_callaudioroutestatemachine_flags.aconfig
@@ -89,6 +89,17 @@
bug: "315865533"
}
+# OWNER=tgunn TARGET=24Q3
+flag {
+ name: "dont_use_communication_device_tracker"
+ namespace: "telecom"
+ description: "Do not use the communication device tracker with useRefactoredAudioRouteSwitching."
+ bug: "346472575"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
# OWNER=pmadapurmath TARGET=24Q3
flag {
name: "resolve_switching_bt_devices_computation"
@@ -129,3 +140,14 @@
purpose: PURPOSE_BUGFIX
}
}
+
+# OWNER=tgunn TARGET=25Q2
+flag {
+ name: "only_clear_communication_device_on_inactive"
+ namespace: "telecom"
+ description: "Only clear the communication device when transitioning to an inactive route."
+ bug: "376781369"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/flags/telecom_headless_system_user_mode.aconfig b/flags/telecom_headless_system_user_mode.aconfig
index d9636a0..4135794 100644
--- a/flags/telecom_headless_system_user_mode.aconfig
+++ b/flags/telecom_headless_system_user_mode.aconfig
@@ -23,4 +23,16 @@
metadata {
purpose: PURPOSE_BUGFIX
}
+}
+
+# OWNER=grantmenke TARGET=25Q2
+flag {
+ name: "telecom_app_label_proxy_hsum_aware"
+ is_exported: true
+ namespace: "telecom"
+ description: "Support HSUM mode by ensuring AppLableProxy is multiuser aware."
+ bug: "321817633"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
}
\ No newline at end of file
diff --git a/flags/telecom_resolve_hidden_dependencies.aconfig b/flags/telecom_resolve_hidden_dependencies.aconfig
index a120b85..e5bb1fb 100644
--- a/flags/telecom_resolve_hidden_dependencies.aconfig
+++ b/flags/telecom_resolve_hidden_dependencies.aconfig
@@ -16,4 +16,5 @@
description: "Fixed read only flag used for setting up BlockedNumbersManager to be retrieved via context"
bug: "325049252"
is_fixed_read_only: true
+ is_exported: true
}
diff --git a/flags/telecom_ringer_flag_declarations.aconfig b/flags/telecom_ringer_flag_declarations.aconfig
index 6517e0f..f954b09 100644
--- a/flags/telecom_ringer_flag_declarations.aconfig
+++ b/flags/telecom_ringer_flag_declarations.aconfig
@@ -15,4 +15,16 @@
namespace: "telecom"
description: "Gates whether to ensure that when a user is in their car, they are able to hear ringing for an incoming call."
bug: "348708398"
+}
+
+
+# OWNER=tjstuart TARGET=25Q1
+flag {
+ name: "get_ringer_mode_anom_report"
+ namespace: "telecom"
+ description: "getRingerMode & getRingerModeInternal should return the same val when dnd is off"
+ bug: "307389562"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
}
\ No newline at end of file
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index d8fc473..4aeceef 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -72,7 +72,7 @@
<string name="block_button" msgid="485080149164258770">"रोक्नुहोस्"</string>
<string name="non_primary_user" msgid="315564589279622098">"यन्त्रको मालिकले रोकिएका नम्बरहरूलाई हेर्न र व्यवस्थापन गर्न सक्छ।"</string>
<string name="delete_icon_description" msgid="5335959254954774373">"अनब्लक गर्नुहोस्"</string>
- <string name="blocked_numbers_butter_bar_title" msgid="582982373755950791">"रोक लगाउने काम अस्थायी रूपमा निष्क्रिय छ"</string>
+ <string name="blocked_numbers_butter_bar_title" msgid="582982373755950791">"रोक लगाउने काम अस्थायी रूपमा अफ छ"</string>
<string name="blocked_numbers_butter_bar_body" msgid="1261213114919301485">"तपाईँले आपत्कालीन नम्बरमा डायल गरेपछि वा टेक्स्ट म्यासेज पठाएपछि आपत्कालीन सेवाहरूले तपाईँलाई सम्पर्क गर्न सकून् भन्ने कुरा सुनिश्चित गर्न कलमाथिको अवरोध निष्क्रिय गरिन्छ।"</string>
<string name="blocked_numbers_butter_bar_button" msgid="2704456308072489793">"अब पुन:-अन गर्नुहोस्"</string>
<string name="blocked_numbers_number_blocked_message" msgid="4314736791180919167">"<xliff:g id="BLOCKED_NUMBER">%1$s</xliff:g> माथि रोक लगाइयो"</string>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index 831f260..96ee0e8 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -109,7 +109,7 @@
<string name="phone_settings_call_blocking_txt" msgid="7311523114822507178">"ਕਾਲ ਬਲਾਕ ਕਰਨਾ"</string>
<string name="phone_settings_number_not_in_contact_txt" msgid="2602249106007265757">"ਨੰਬਰ ਜੋ ਤੁਹਾਡੇ ਸੰਪਰਕਾਂ ਵਿੱਚ ਨਹੀਂ ਹਨ"</string>
<string name="phone_settings_number_not_in_contact_summary_txt" msgid="963327038085718969">"ਉਹ ਨੰਬਰ ਬਲਾਕ ਕਰੋ ਜੋ ਤੁਹਾਡੇ ਸੰਪਰਕਾਂ ਵਿੱਚ ਨਹੀਂ ਹਨ"</string>
- <string name="phone_settings_private_num_txt" msgid="6339272760338475619">"ਨਿੱਜੀ"</string>
+ <string name="phone_settings_private_num_txt" msgid="6339272760338475619">"ਪ੍ਰਾਈਵੇਟ"</string>
<string name="phone_settings_private_num_summary_txt" msgid="6755758240544021037">"ਉਹ ਕਾਲਰ ਬਲਾਕ ਕਰੋ ਜਿਨ੍ਹਾਂ ਦਾ ਨੰਬਰ ਨਹੀਂ ਦਿਖਾਈ ਦਿੰਦਾ ਹੈ"</string>
<string name="phone_settings_payphone_txt" msgid="5003987966052543965">"ਜਨਤਕ ਫ਼ੋਨ"</string>
<string name="phone_settings_payphone_summary_txt" msgid="3936631076065563665">"ਜਨਤਕ ਫ਼ੋਨਾਂ ਵਾਲੀਆਂ ਕਾਲਾਂ ਬਲਾਕ ਕਰੋ"</string>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index 1e8b027..e302ea6 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -50,8 +50,8 @@
<string name="outgoing_call_not_allowed_no_permission" msgid="8590468836581488679">"Este aplicativo não pode fazer chamadas sem a permissão do smartphone."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="7665135102566099778">"Para realizar uma chamada, digite um número válido."</string>
<string name="duplicate_video_call_not_allowed" msgid="5754746140185781159">"No momento, não é possível adicionar a chamada."</string>
- <string name="no_vm_number" msgid="2179959110602180844">"Número correio de voz ausente"</string>
- <string name="no_vm_number_msg" msgid="1339245731058529388">"Não há um número correio de voz armazenado no chip."</string>
+ <string name="no_vm_number" msgid="2179959110602180844">"Número do correio de voz ausente"</string>
+ <string name="no_vm_number_msg" msgid="1339245731058529388">"Não há um número do correio de voz armazenado no chip."</string>
<string name="add_vm_number_str" msgid="5179510133063168998">"Adicionar número"</string>
<string name="change_default_dialer_dialog_title" msgid="5861469279421508060">"Usar o <xliff:g id="NEW_APP">%s</xliff:g> como seu app de telefone padrão?"</string>
<string name="change_default_dialer_dialog_affirmative" msgid="8604665314757739550">"Definir padrão"</string>
diff --git a/src/com/android/server/telecom/AppLabelProxy.java b/src/com/android/server/telecom/AppLabelProxy.java
index 7c00f28..c4d83dd 100644
--- a/src/com/android/server/telecom/AppLabelProxy.java
+++ b/src/com/android/server/telecom/AppLabelProxy.java
@@ -16,8 +16,11 @@
package com.android.server.telecom;
+import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import com.android.server.telecom.flags.FeatureFlags;
+import android.os.UserHandle;
import android.telecom.Log;
/**
@@ -30,15 +33,34 @@
class Util {
/**
* Default impl of getAppLabel.
- * @param pm PackageManager instance
+ * @param context Context instance that is not necessarily associated with the correct user.
+ * @param userHandle UserHandle instance of the user that is associated with the app.
* @param packageName package name to look up.
*/
- public static CharSequence getAppLabel(PackageManager pm, String packageName) {
+ public static CharSequence getAppLabel(Context context, UserHandle userHandle,
+ String packageName, FeatureFlags featureFlags) {
try {
- ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
- CharSequence result = pm.getApplicationLabel(info);
- Log.i(LOG_TAG, "package %s: name is %s", packageName, result);
- return result;
+ if (featureFlags.telecomAppLabelProxyHsumAware()){
+ Context userContext = context.createContextAsUser(userHandle, 0 /* flags */);
+ PackageManager userPackageManager = userContext.getPackageManager();
+ if (userPackageManager == null) {
+ Log.w(LOG_TAG, "Could not determine app label since PackageManager is "
+ + "null. Package name is %s", packageName);
+ return null;
+ }
+ ApplicationInfo info = userPackageManager.getApplicationInfo(packageName, 0);
+ CharSequence result = userPackageManager.getApplicationLabel(info);
+ Log.i(LOG_TAG, "package %s: name is %s for user = %s", packageName, result,
+ userHandle.toString());
+ return result;
+ } else {
+ // Legacy code path:
+ PackageManager pm = context.getPackageManager();
+ ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
+ CharSequence result = pm.getApplicationLabel(info);
+ Log.i(LOG_TAG, "package %s: name is %s", packageName, result);
+ return result;
+ }
} catch (PackageManager.NameNotFoundException nnfe) {
Log.w(LOG_TAG, "Could not determine app label. Package name is %s", packageName);
}
@@ -47,5 +69,5 @@
}
}
- CharSequence getAppLabel(String packageName);
+ CharSequence getAppLabel(String packageName, UserHandle userHandle);
}
diff --git a/src/com/android/server/telecom/AudioRoute.java b/src/com/android/server/telecom/AudioRoute.java
index d3ed77d..aaded77 100644
--- a/src/com/android/server/telecom/AudioRoute.java
+++ b/src/com/android/server/telecom/AudioRoute.java
@@ -24,6 +24,7 @@
import android.annotation.IntDef;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothStatusCodes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
@@ -300,7 +301,8 @@
pendingAudioRoute.setCommunicationDeviceType(mAudioRouteType);
}
Log.i(this, "onDestRouteAsPendingRoute: route=%s, "
- + "AudioManager#setCommunicationDevice()=%b", this, result);
+ + "AudioManager#setCommunicationDevice(%s)=%b", this,
+ audioDeviceTypeToString(mInfo.getType()), result);
break;
}
}
@@ -314,13 +316,19 @@
}
}
- // Takes care of cleaning up original audio route (i.e. clearCommunicationDevice,
- // sending SPEAKER_OFF, or disconnecting SCO).
- void onOrigRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
+ /**
+ * Takes care of cleaning up original audio route (i.e. clearCommunicationDevice,
+ * sending SPEAKER_OFF, or disconnecting SCO).
+ * @param wasActive Was the origin route active or not.
+ * @param pendingAudioRoute The pending audio route change we're performing.
+ * @param audioManager Good 'ol audio manager.
+ * @param bluetoothRouteManager The BT route manager.
+ */
+ void onOrigRouteAsPendingRoute(boolean wasActive, PendingAudioRoute pendingAudioRoute,
AudioManager audioManager, BluetoothRouteManager bluetoothRouteManager) {
- Log.i(this, "onOrigRouteAsPendingRoute: active (%b), type (%s)", active,
- DEVICE_TYPE_STRINGS.get(mAudioRouteType));
- if (active) {
+ Log.i(this, "onOrigRouteAsPendingRoute: wasActive (%b), type (%s), pending(%s)", wasActive,
+ DEVICE_TYPE_STRINGS.get(mAudioRouteType), pendingAudioRoute);
+ if (wasActive) {
int result = clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager,
audioManager);
if (mAudioRouteType == TYPE_SPEAKER) {
@@ -389,6 +397,20 @@
return success;
}
+ /**
+ * Clears the communication device; this takes into account the fact that SCO devices require
+ * us to call {@link BluetoothHeadset#disconnectAudio()} rather than
+ * {@link AudioManager#clearCommunicationDevice()}.
+ * As a general rule, if we are transitioning from an active route to another active route, we
+ * do NOT need to call {@link AudioManager#clearCommunicationDevice()}, but if the device is a
+ * legacy SCO device we WILL need to call {@link BluetoothHeadset#disconnectAudio()}. We rely
+ * on the {@link PendingAudioRoute#isActive()} indicator to tell us if the destination route
+ * is going to be active or not.
+ * @param pendingAudioRoute The pending audio route transition we're implementing.
+ * @param bluetoothRouteManager The BT route manager.
+ * @param audioManager The audio manager.
+ * @return -1 if nothing was done, or the result code from the BT SCO disconnect.
+ */
int clearCommunicationDevice(PendingAudioRoute pendingAudioRoute,
BluetoothRouteManager bluetoothRouteManager, AudioManager audioManager) {
// Try to see if there's a previously set device for communication that should be cleared.
@@ -402,9 +424,17 @@
Log.i(this, "clearCommunicationDevice: Disconnecting SCO device.");
result = bluetoothRouteManager.getDeviceManager().disconnectSco();
} else {
- Log.i(this, "clearCommunicationDevice: AudioManager#clearCommunicationDevice, type=%s",
- DEVICE_TYPE_STRINGS.get(pendingAudioRoute.getCommunicationDeviceType()));
- audioManager.clearCommunicationDevice();
+ // Only clear communication device if the destination route will be inactive; route to
+ // route transitions do not require clearing the communication device.
+ boolean onlyClearCommunicationDeviceOnInactive =
+ pendingAudioRoute.getFeatureFlags().onlyClearCommunicationDeviceOnInactive();
+ if (!onlyClearCommunicationDeviceOnInactive
+ || (onlyClearCommunicationDeviceOnInactive && !pendingAudioRoute.isActive())) {
+ Log.i(this,
+ "clearCommunicationDevice: AudioManager#clearCommunicationDevice, type=%s",
+ DEVICE_TYPE_STRINGS.get(pendingAudioRoute.getCommunicationDeviceType()));
+ audioManager.clearCommunicationDevice();
+ }
}
if (result == BluetoothStatusCodes.SUCCESS) {
@@ -430,4 +460,23 @@
pendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_ON, null));
}
}
+
+ /**
+ * Get a human readable (for logs) version of an an audio device type.
+ * @param type the device type
+ * @return the human readable string
+ */
+ private static String audioDeviceTypeToString(int type) {
+ return switch (type) {
+ case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> "earpiece";
+ case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> "speaker";
+ case AudioDeviceInfo.TYPE_BUS -> "bus(auto speaker)";
+ case AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> "bt sco";
+ case AudioDeviceInfo.TYPE_BLE_HEADSET -> "bt le";
+ case AudioDeviceInfo.TYPE_HEARING_AID -> "bt hearing aid";
+ case AudioDeviceInfo.TYPE_USB_HEADSET -> "usb headset";
+ case AudioDeviceInfo.TYPE_WIRED_HEADSET -> "wired headset";
+ default -> Integer.toString(type);
+ };
+ }
}
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index df31e02..9e566e2 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -80,6 +80,7 @@
import com.android.server.telecom.stats.CallFailureCause;
import com.android.server.telecom.stats.CallStateChangedAtomWriter;
import com.android.server.telecom.ui.ToastFactory;
+import com.android.server.telecom.callsequencing.CallTransaction;
import com.android.server.telecom.callsequencing.TransactionManager;
import com.android.server.telecom.callsequencing.VerifyCallStateChangeTransaction;
import com.android.server.telecom.callsequencing.CallTransactionResult;
@@ -2832,20 +2833,20 @@
}
@VisibleForTesting
- public void disconnect() {
- disconnect(0);
+ public CompletableFuture<Boolean> disconnect() {
+ return disconnect(0);
}
- public void disconnect(String reason) {
- disconnect(0, reason);
+ public CompletableFuture<Boolean> disconnect(String reason) {
+ return disconnect(0, reason);
}
/**
* Attempts to disconnect the call through the connection service.
*/
@VisibleForTesting
- public void disconnect(long disconnectionTimeout) {
- disconnect(disconnectionTimeout, "internal" /** reason */);
+ public CompletableFuture<Boolean> disconnect(long disconnectionTimeout) {
+ return disconnect(disconnectionTimeout, "internal" /* reason */);
}
/**
@@ -2855,16 +2856,24 @@
* as TelecomManager.
*/
@VisibleForTesting
- public void disconnect(long disconnectionTimeout, String reason) {
+ public CompletableFuture<Boolean> disconnect(long disconnectionTimeout,
+ String reason) {
Log.addEvent(this, LogUtils.Events.REQUEST_DISCONNECT, reason);
// Track that the call is now locally disconnecting.
setLocallyDisconnecting(true);
maybeSetCallAsDisconnectingChild();
+ CompletableFuture<Boolean> disconnectFutureHandler =
+ CompletableFuture.completedFuture(false);
if (mState == CallState.NEW || mState == CallState.SELECT_PHONE_ACCOUNT ||
mState == CallState.CONNECTING) {
Log.i(this, "disconnect: Aborting call %s", getId());
+ if (mFlags.enableCallSequencing()) {
+ disconnectFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall(
+ false /* shouldDisconnectUponTimeout */, "disconnect",
+ CallState.DISCONNECTED, CallState.ABORTED);
+ }
abort(disconnectionTimeout);
} else if (mState != CallState.ABORTED && mState != CallState.DISCONNECTED) {
if (mState == CallState.AUDIO_PROCESSING && !hasGoneActiveBefore()) {
@@ -2876,7 +2885,8 @@
setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.MISSED));
}
if (mTransactionalService != null) {
- mTransactionalService.onDisconnect(this, getDisconnectCause());
+ disconnectFutureHandler = mTransactionalService.onDisconnect(this,
+ getDisconnectCause());
Log.i(this, "Send Disconnect to transactional service for call");
} else if (mConnectionService == null) {
Log.e(this, new Exception(), "disconnect() request on a call without a"
@@ -2887,9 +2897,15 @@
// confirms that the call was actually disconnected. Only then is the
// association between call and connection service severed, see
// {@link CallsManager#markCallAsDisconnected}.
+ if (mFlags.enableCallSequencing()) {
+ disconnectFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall(
+ false /* shouldDisconnectUponTimeout */, "disconnect",
+ CallState.DISCONNECTED);
+ }
mConnectionService.disconnect(this);
}
}
+ return disconnectFutureHandler;
}
void abort(long disconnectionTimeout) {
@@ -2932,29 +2948,35 @@
* @param videoState The video state in which to answer the call.
*/
@VisibleForTesting
- public void answer(int videoState) {
+ public CompletableFuture<Boolean> answer(int videoState) {
+ CompletableFuture<Boolean> answerCallFuture = CompletableFuture.completedFuture(false);
// Check to verify that the call is still in the ringing state. A call can change states
// between the time the user hits 'answer' and Telecom receives the command.
if (isRinging("answer")) {
+ Log.addEvent(this, LogUtils.Events.REQUEST_ACCEPT);
if (!isVideoCallingSupportedByPhoneAccount() && VideoProfile.isVideo(videoState)) {
// Video calling is not supported, yet the InCallService is attempting to answer as
// video. We will simply answer as audio-only.
videoState = VideoProfile.STATE_AUDIO_ONLY;
}
// At this point, we are asking the connection service to answer but we don't assume
- // that it will work. Instead, we wait until confirmation from the connectino service
+ // that it will work. Instead, we wait until confirmation from the connection service
// that the call is in a non-STATE_RINGING state before changing the UI. See
// {@link ConnectionServiceAdapter#setActive} and other set* methods.
if (mConnectionService != null) {
+ if (mFlags.enableCallSequencing()) {
+ answerCallFuture = awaitCallStateChangeAndMaybeDisconnectCall(
+ false /* shouldDisconnectUponTimeout */, "answer", CallState.ACTIVE);
+ }
mConnectionService.answer(this, videoState);
} else if (mTransactionalService != null) {
- mTransactionalService.onAnswer(this, videoState);
+ return mTransactionalService.onAnswer(this, videoState);
} else {
Log.e(this, new NullPointerException(),
"answer call failed due to null CS callId=%s", getId());
}
- Log.addEvent(this, LogUtils.Events.REQUEST_ACCEPT);
}
+ return answerCallFuture;
}
/**
@@ -3034,74 +3056,101 @@
* if the reject is initiated from an API such as TelecomManager.
*/
@VisibleForTesting
- public void reject(boolean rejectWithMessage, String textMessage, String reason) {
+ public CompletableFuture<Boolean> reject(boolean rejectWithMessage,
+ String textMessage, String reason) {
+ CompletableFuture<Boolean> rejectFutureHandler = CompletableFuture.completedFuture(false);
if (mState == CallState.SIMULATED_RINGING) {
+ Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, reason);
// This handles the case where the user manually rejects a call that's in simulated
// ringing. Since the call is already active on the connectionservice side, we want to
// hangup, not reject.
setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.REJECTED));
if (mTransactionalService != null) {
- mTransactionalService.onDisconnect(this,
+ return mTransactionalService.onDisconnect(this,
new DisconnectCause(DisconnectCause.REJECTED));
} else if (mConnectionService != null) {
+ if (mFlags.enableCallSequencing()) {
+ rejectFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall(
+ false /* shouldDisconnectUponTimeout */, "reject",
+ CallState.DISCONNECTED);
+ }
mConnectionService.disconnect(this);
+ return rejectFutureHandler;
} else {
Log.e(this, new NullPointerException(),
"reject call failed due to null CS callId=%s", getId());
}
- Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, reason);
} else if (isRinging("reject") || isAnswered("reject")) {
+ Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, reason);
// Ensure video state history tracks video state at time of rejection.
mVideoStateHistory |= mVideoState;
if (mTransactionalService != null) {
- mTransactionalService.onDisconnect(this,
+ return mTransactionalService.onDisconnect(this,
new DisconnectCause(DisconnectCause.REJECTED));
} else if (mConnectionService != null) {
+ if (mFlags.enableCallSequencing()) {
+ rejectFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall(
+ false /* shouldDisconnectUponTimeout */, "reject",
+ CallState.DISCONNECTED);
+ }
mConnectionService.reject(this, rejectWithMessage, textMessage);
+ return rejectFutureHandler;
} else {
Log.e(this, new NullPointerException(),
"reject call failed due to null CS callId=%s", getId());
}
- Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, reason);
}
+ return rejectFutureHandler;
}
/**
* Reject this Telecom call with the user-indicated reason.
* @param rejectReason The user-indicated reason fore rejecting the call.
*/
- public void reject(@android.telecom.Call.RejectReason int rejectReason) {
+ public CompletableFuture<Boolean> reject(@android.telecom.Call.RejectReason int rejectReason) {
+ CompletableFuture<Boolean> rejectFutureHandler = CompletableFuture.completedFuture(false);
if (mState == CallState.SIMULATED_RINGING) {
+ Log.addEvent(this, LogUtils.Events.REQUEST_REJECT);
// This handles the case where the user manually rejects a call that's in simulated
// ringing. Since the call is already active on the connectionservice side, we want to
// hangup, not reject.
// Since its simulated reason we can't pass along the reject reason.
setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.REJECTED));
if (mTransactionalService != null) {
- mTransactionalService.onDisconnect(this,
+ return mTransactionalService.onDisconnect(this,
new DisconnectCause(DisconnectCause.REJECTED));
} else if (mConnectionService != null) {
+ if (mFlags.enableCallSequencing()) {
+ rejectFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall(
+ false /* shouldDisconnectUponTimeout */, "reject",
+ CallState.DISCONNECTED);
+ }
mConnectionService.disconnect(this);
} else {
Log.e(this, new NullPointerException(),
"reject call failed due to null CS callId=%s", getId());
}
- Log.addEvent(this, LogUtils.Events.REQUEST_REJECT);
} else if (isRinging("reject") || isAnswered("reject")) {
+ Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, rejectReason);
// Ensure video state history tracks video state at time of rejection.
mVideoStateHistory |= mVideoState;
if (mTransactionalService != null) {
- mTransactionalService.onDisconnect(this,
+ return mTransactionalService.onDisconnect(this,
new DisconnectCause(DisconnectCause.REJECTED));
} else if (mConnectionService != null) {
+ if (mFlags.enableCallSequencing()) {
+ rejectFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall(
+ false /* shouldDisconnectUponTimeout */, "reject",
+ CallState.DISCONNECTED);
+ }
mConnectionService.rejectWithReason(this, rejectReason);
} else {
Log.e(this, new NullPointerException(),
"reject call failed due to null CS callId=%s", getId());
}
- Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, rejectReason);
}
+ return rejectFutureHandler;
}
/**
@@ -3151,41 +3200,46 @@
* Puts the call on hold if it is currently active.
*/
@VisibleForTesting
- public void hold() {
- hold(null /* reason */);
+ public CompletableFuture<Boolean> hold() {
+ return hold(null /* reason */);
}
/**
* This method requests the ConnectionService or TransactionalService hosting the call to put
* the call on hold
*/
- public void hold(String reason) {
+ public CompletableFuture<Boolean> hold(String reason) {
+ CompletableFuture<Boolean> holdFutureHandler = CompletableFuture.completedFuture(false);
if (mState == CallState.ACTIVE) {
+ Log.addEvent(this, LogUtils.Events.REQUEST_HOLD, reason);
if (mTransactionalService != null) {
- mTransactionalService.onSetInactive(this);
+ return mTransactionalService.onSetInactive(this);
} else if (mConnectionService != null) {
- if (mFlags.transactionalCsVerifier()) {
- awaitCallStateChangeAndMaybeDisconnectCall(CallState.ON_HOLD, isSelfManaged(),
- "hold");
+ if (mFlags.transactionalCsVerifier() || mFlags.enableCallSequencing()) {
+ holdFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall(isSelfManaged(),
+ "hold", CallState.ON_HOLD);
}
mConnectionService.hold(this);
+ return holdFutureHandler;
} else {
Log.e(this, new NullPointerException(),
"hold call failed due to null CS callId=%s", getId());
}
- Log.addEvent(this, LogUtils.Events.REQUEST_HOLD, reason);
}
+ return holdFutureHandler;
}
/**
* helper that can be used for any callback that requests a call state change and wants to
* verify the change
*/
- public void awaitCallStateChangeAndMaybeDisconnectCall(int targetCallState,
- boolean shouldDisconnectUponTimeout, String callingMethod) {
+ public CompletableFuture<Boolean> awaitCallStateChangeAndMaybeDisconnectCall(
+ boolean shouldDisconnectUponTimeout, String callingMethod, int... targetCallStates) {
TransactionManager tm = TransactionManager.getInstance();
- tm.addTransaction(new VerifyCallStateChangeTransaction(mCallsManager.getLock(),
- this, targetCallState), new OutcomeReceiver<>() {
+ CallTransaction callTransaction = new VerifyCallStateChangeTransaction(
+ mCallsManager.getLock(), this, targetCallStates);
+ return tm.addTransaction(callTransaction,
+ new OutcomeReceiver<>() {
@Override
public void onResult(CallTransactionResult result) {
Log.i(this, "awaitCallStateChangeAndMaybeDisconnectCall: %s: onResult:"
@@ -3210,22 +3264,29 @@
* Releases the call from hold if it is currently active.
*/
@VisibleForTesting
- public void unhold() {
- unhold(null /* reason */);
+ public CompletableFuture<Boolean> unhold() {
+ return unhold(null /* reason */);
}
- public void unhold(String reason) {
+ public CompletableFuture<Boolean> unhold(String reason) {
+ CompletableFuture<Boolean> unholdFutureHandler = CompletableFuture.completedFuture(false);
if (mState == CallState.ON_HOLD) {
+ Log.addEvent(this, LogUtils.Events.REQUEST_UNHOLD, reason);
if (mTransactionalService != null){
- mTransactionalService.onSetActive(this);
+ return mTransactionalService.onSetActive(this);
} else if (mConnectionService != null){
+ if (mFlags.enableCallSequencing()) {
+ unholdFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall(
+ false /* shouldDisconnectUponTimeout */, "unhold", CallState.ACTIVE);
+ }
mConnectionService.unhold(this);
+ return unholdFutureHandler;
} else {
Log.e(this, new NullPointerException(),
"unhold call failed due to null CS callId=%s", getId());
}
- Log.addEvent(this, LogUtils.Events.REQUEST_UNHOLD, reason);
}
+ return unholdFutureHandler;
}
/** Checks if this is a live call or not. */
diff --git a/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java b/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
index 8d5f9fd..84f7d8f 100644
--- a/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
+++ b/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
@@ -251,6 +251,7 @@
}
// Clear device and reset locally saved device type.
+ Log.i(this, "clearCommunicationDevice: AudioManager#clearCommunicationDevice()");
mAudioManager.clearCommunicationDevice();
mAudioDeviceType = sAUDIO_DEVICE_TYPE_INVALID;
diff --git a/src/com/android/server/telecom/CallAudioModeStateMachine.java b/src/com/android/server/telecom/CallAudioModeStateMachine.java
index e149bdd..d1fd564 100644
--- a/src/com/android/server/telecom/CallAudioModeStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioModeStateMachine.java
@@ -334,9 +334,15 @@
mAudioManager.abandonAudioFocusForCall();
// Clear requested communication device after the call ends.
if (mFeatureFlags.clearCommunicationDeviceAfterAudioOpsComplete()) {
- mCommunicationDeviceTracker.clearCommunicationDevice(
- mCommunicationDeviceTracker
- .getCurrentLocallyRequestedCommunicationDevice());
+ // Oh flags! If we're using the refactored audio route switching, we should
+ // not be using the communication device tracker; that is exclusively for
+ // the old code path.
+ if (!mFeatureFlags.dontUseCommunicationDeviceTracker()
+ || !mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ mCommunicationDeviceTracker.clearCommunicationDevice(
+ mCommunicationDeviceTracker
+ .getCurrentLocallyRequestedCommunicationDevice());
+ }
}
return HANDLED;
default:
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index cf88cab..495f872 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -570,7 +570,8 @@
}
// override pending route while keep waiting for still pending messages for the
// previous pending route
- mPendingAudioRoute.setOrigRoute(mIsActive, mPendingAudioRoute.getDestRoute());
+ mPendingAudioRoute.setOrigRoute(mIsActive /* origin */,
+ mPendingAudioRoute.getDestRoute(), active /* dest */);
} else {
if (mCurrentRoute.equals(destRoute) && (mIsActive == active)) {
return;
@@ -579,10 +580,12 @@
mIsActive, destRoute, active);
// route to pending route
if (getCallSupportedRoutes().contains(mCurrentRoute)) {
- mPendingAudioRoute.setOrigRoute(mIsActive, mCurrentRoute);
+ mPendingAudioRoute.setOrigRoute(mIsActive /* origin */, mCurrentRoute,
+ active /* dest */);
} else {
// Avoid waiting for pending messages for an unavailable route
- mPendingAudioRoute.setOrigRoute(mIsActive, DUMMY_ROUTE);
+ mPendingAudioRoute.setOrigRoute(mIsActive /* origin */, DUMMY_ROUTE,
+ active /* dest */);
}
mIsPending = true;
}
@@ -803,7 +806,6 @@
if (bluetoothRoute != null) {
Log.i(this, "request to route to bluetooth route: %s (active=%b)", bluetoothRoute,
mIsActive);
- updateActiveBluetoothDevice(new Pair<>(type, deviceAddress));
routeTo(mIsActive, bluetoothRoute);
} else {
Log.i(this, "request to route to unavailable bluetooth route - type (%s), address (%s)",
@@ -847,10 +849,6 @@
// Fallback to an available route excluding the previously active device.
routeTo(mIsActive, getBaseRoute(true, previouslyActiveDeviceAddress));
}
- // Clear out the active device for the BT audio type.
- if (mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()) {
- updateActiveBluetoothDevice(new Pair(type, null));
- }
}
private void handleMuteChanged(boolean mute) {
@@ -986,12 +984,16 @@
* @return {@link AudioRoute} of the BT device.
*/
private AudioRoute getArbitraryBluetoothDevice() {
- if (mActiveBluetoothDevice != null) {
- return getBluetoothRoute(mActiveBluetoothDevice.first, mActiveBluetoothDevice.second);
- } else if (!mBluetoothRoutes.isEmpty()) {
- return mBluetoothRoutes.keySet().stream().toList().get(mBluetoothRoutes.size() - 1);
+ synchronized (mLock) {
+ if (mActiveBluetoothDevice != null) {
+ return getBluetoothRoute(
+ mActiveBluetoothDevice.first, mActiveBluetoothDevice.second);
+ } else if (!mBluetoothRoutes.isEmpty()) {
+ return mBluetoothRoutes.keySet().stream().toList()
+ .get(mBluetoothRoutes.size() - 1);
+ }
+ return null;
}
- return null;
}
private void handleSwitchHeadset() {
@@ -1026,15 +1028,28 @@
// If SCO is once again connected or there's a pending message for BT_AUDIO_CONNECTED, then
// we know that the device has reconnected or is in the middle of connecting. Ignore routing
// out of this BT device.
- if (mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue() && areExcludedBtAndDestBtSame
+ boolean isExcludedDeviceConnectingOrConnected = areExcludedBtAndDestBtSame
&& (mIsScoAudioConnected || mPendingAudioRoute.getPendingMessages()
- .contains(btDevicePendingMsg))) {
- Log.i(this, "BT device with address (%s) is currently connecting/connected. "
- + "Ignore route switch.");
- } else {
- routeTo(mIsActive, calculateBaselineRoute(isExplicitUserRequest, includeBluetooth,
- btAddressToExclude));
+ .contains(btDevicePendingMsg));
+ // Check if the pending audio route or current route is already different from the route
+ // including the BT device that should be excluded from route selection.
+ boolean isCurrentOrDestRouteDifferent = btAddressToExclude != null
+ && ((mIsPending && !btAddressToExclude.equals(mPendingAudioRoute.getDestRoute()
+ .getBluetoothAddress())) || (!mIsPending && !btAddressToExclude.equals(
+ mCurrentRoute.getBluetoothAddress())));
+ if (mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()) {
+ if (isExcludedDeviceConnectingOrConnected) {
+ Log.i(this, "BT device with address (%s) is currently connecting/connected. "
+ + "Ignoring route switch.", btAddressToExclude);
+ return;
+ } else if (isCurrentOrDestRouteDifferent) {
+ Log.i(this, "Current or pending audio route isn't routed to device with address "
+ + "(%s). Ignoring route switch.", btAddressToExclude);
+ return;
+ }
}
+ routeTo(mIsActive, calculateBaselineRoute(isExplicitUserRequest, includeBluetooth,
+ btAddressToExclude));
}
private void handleSpeakerOn() {
@@ -1045,7 +1060,8 @@
mStatusBarNotifier.notifySpeakerphone(mCallsManager.hasAnyCalls());
} else {
if (mSpeakerDockRoute != null && getCallSupportedRoutes().contains(mSpeakerDockRoute)
- && mSpeakerDockRoute.getType() == AudioRoute.TYPE_SPEAKER) {
+ && mSpeakerDockRoute.getType() == AudioRoute.TYPE_SPEAKER
+ && mCurrentRoute.getType() != AudioRoute.TYPE_SPEAKER) {
routeTo(mIsActive, mSpeakerDockRoute);
// Since the route switching triggered by this message, we need to manually send it
// again so that we won't stuck in the pending route
@@ -1444,8 +1460,14 @@
continue;
}
// Check if the most recently active device is a watch device.
- if (i == (bluetoothRoutes.size() - 1) && device.equals(mCallAudioState
- .getActiveBluetoothDevice()) && mBluetoothRouteManager.isWatch(device)) {
+ boolean isActiveDevice;
+ synchronized (mLock) {
+ isActiveDevice = mActiveBluetoothDevice != null
+ && device.getAddress().equals(mActiveBluetoothDevice.second);
+ }
+ if (i == (bluetoothRoutes.size() - 1) && mBluetoothRouteManager.isWatch(device)
+ && (device.equals(mCallAudioState.getActiveBluetoothDevice())
+ || isActiveDevice)) {
Log.i(this, "getActiveWatchOrNonWatchDeviceRoute: Routing to active watch - %s",
bluetoothRoutes.get(0));
return bluetoothRoutes.get(0);
@@ -1556,29 +1578,32 @@
* address of the device.
*/
public void updateActiveBluetoothDevice(Pair<Integer, String> device) {
- mActiveDeviceCache.put(device.first, device.second);
- // Update most recently active device if address isn't null (meaning some device is active).
- if (device.second != null) {
- mActiveBluetoothDevice = device;
- } else {
- // If a device was removed, check to ensure that no other device is still considered
- // active.
- boolean hasActiveDevice = false;
- List<Map.Entry<Integer, String>> activeBtDevices = new ArrayList<>(
- mActiveDeviceCache.entrySet());
- for (Map.Entry<Integer,String> activeDevice : activeBtDevices) {
- Integer btAudioType = activeDevice.getKey();
- String address = activeDevice.getValue();
- if (address != null) {
- hasActiveDevice = true;
- if (mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()) {
- mActiveBluetoothDevice = new Pair<>(btAudioType, address);
+ synchronized (mLock) {
+ mActiveDeviceCache.put(device.first, device.second);
+ // Update most recently active device if address isn't null (meaning
+ // some device is active).
+ if (device.second != null) {
+ mActiveBluetoothDevice = device;
+ } else {
+ // If a device was removed, check to ensure that no other device is
+ //still considered active.
+ boolean hasActiveDevice = false;
+ List<Map.Entry<Integer, String>> activeBtDevices =
+ new ArrayList<>(mActiveDeviceCache.entrySet());
+ for (Map.Entry<Integer, String> activeDevice : activeBtDevices) {
+ Integer btAudioType = activeDevice.getKey();
+ String address = activeDevice.getValue();
+ if (address != null) {
+ hasActiveDevice = true;
+ if (mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()) {
+ mActiveBluetoothDevice = new Pair<>(btAudioType, address);
+ }
+ break;
}
- break;
}
- }
- if (!hasActiveDevice) {
- mActiveBluetoothDevice = null;
+ if (!hasActiveDevice) {
+ mActiveBluetoothDevice = null;
+ }
}
}
}
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 540c152..712c6a9 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -27,6 +27,8 @@
import static android.provider.CallLog.Calls.USER_MISSED_NOT_RUNNING;
import static android.provider.CallLog.Calls.USER_MISSED_NO_ANSWER;
import static android.provider.CallLog.Calls.USER_MISSED_SHORT_RING;
+import static android.telecom.CallAttributes.DIRECTION_INCOMING;
+import static android.telecom.CallAttributes.DIRECTION_OUTGOING;
import static android.telecom.TelecomManager.ACTION_POST_CALL;
import static android.telecom.TelecomManager.DURATION_LONG;
import static android.telecom.TelecomManager.DURATION_MEDIUM;
@@ -132,6 +134,8 @@
import com.android.server.telecom.callfiltering.IncomingCallFilterGraphProvider;
import com.android.server.telecom.callredirection.CallRedirectionProcessor;
import com.android.server.telecom.callsequencing.CallSequencingController;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.voip.IncomingCallTransaction;
import com.android.server.telecom.components.ErrorDialogActivity;
import com.android.server.telecom.components.TelecomBroadcastReceiver;
import com.android.server.telecom.callsequencing.CallsManagerCallSequencingAdapter;
@@ -324,7 +328,7 @@
public static final String TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_MSG =
"Telephony has a default MO acct but Telecom prompted user for MO";
- private static final int[] OUTGOING_CALL_STATES =
+ public static final int[] OUTGOING_CALL_STATES =
{CallState.CONNECTING, CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING,
CallState.PULLING};
@@ -703,7 +707,7 @@
ringtoneFactory, systemVibrator,
new Ringer.VibrationEffectProxy(), mInCallController,
mContext.getSystemService(NotificationManager.class),
- accessibilityManagerAdapter, featureFlags);
+ accessibilityManagerAdapter, featureFlags, mAnomalyReporter);
if (featureFlags.telecomResolveHiddenDependencies()) {
// This is now deprecated
mCallRecordingTonePlayer = null;
@@ -744,8 +748,8 @@
? mContext.getSystemService(BlockedNumbersManager.class)
: null;
mCallSequencingAdapter = new CallsManagerCallSequencingAdapter(this,
- new CallSequencingController(this, mFeatureFlags.enableCallSequencing()),
- mFeatureFlags.enableCallSequencing());
+ new CallSequencingController(this, mContext,
+ mFeatureFlags), mFeatureFlags);
if (mFeatureFlags.useImprovedListenerOrder()) {
mListeners.add(mInCallController);
@@ -939,8 +943,8 @@
String defaultDialerPackageName = telecomManager.getDefaultDialerPackage(userHandle);
String userChosenPackageName = getRoleManagerAdapter().
getDefaultCallScreeningApp(userHandle);
- AppLabelProxy appLabelProxy = packageName -> AppLabelProxy.Util.getAppLabel(
- mContext.getPackageManager(), packageName);
+ AppLabelProxy appLabelProxy = (packageName, user) -> AppLabelProxy.Util.getAppLabel(
+ mContext, user, packageName, mFeatureFlags);
ParcelableCallUtils.Converter converter = new ParcelableCallUtils.Converter();
IncomingCallFilterGraph graph = mIncomingCallFilterGraphProvider.createGraph(incomingCall,
@@ -1874,6 +1878,37 @@
originalIntent, callingPackage, false);
}
+ /**
+ * Creates a transaction representing either the outgoing or incoming transactional call.
+ * @param callId The call id associated with the call.
+ * @param callAttributes The call attributes associated with the call.
+ * @param extras The extras that are associated with the call.
+ * @param callingPackage The calling package representing where the request was invoked from.
+ * @return The {@link CompletableFuture<CallTransaction>} that encompasses the request to
+ * place/receive the transactional call.
+ */
+ public CompletableFuture<CallTransaction> createTransactionalCall(String callId,
+ CallAttributes callAttributes, Bundle extras, String callingPackage) {
+ CompletableFuture<CallTransaction> transaction;
+ // create transaction based on the call direction
+ switch (callAttributes.getDirection()) {
+ case DIRECTION_OUTGOING:
+ transaction = mCallSequencingAdapter.createTransactionalOutgoingCall(callId,
+ callAttributes, extras, callingPackage);
+ break;
+ case DIRECTION_INCOMING:
+ transaction = CompletableFuture.completedFuture(new IncomingCallTransaction(
+ callId, callAttributes, this, extras, mFeatureFlags));
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("Invalid Call Direction. "
+ + "Was [%d] but should be within [%d,%d]",
+ callAttributes.getDirection(), DIRECTION_INCOMING,
+ DIRECTION_OUTGOING));
+ }
+ return transaction;
+ }
+
private String generateNextCallId(Bundle extras) {
if (extras != null && extras.containsKey(TelecomManager.TRANSACTION_CALL_ID_KEY)) {
return extras.getString(TelecomManager.TRANSACTION_CALL_ID_KEY);
@@ -2592,8 +2627,8 @@
theCall,
new AppLabelProxy() {
@Override
- public CharSequence getAppLabel(String packageName) {
- return Util.getAppLabel(mContext.getPackageManager(), packageName);
+ public CharSequence getAppLabel(String packageName, UserHandle userHandle) {
+ return Util.getAppLabel(mContext, userHandle, packageName, mFeatureFlags);
}
}).process();
future.thenApply( v -> {
@@ -3214,7 +3249,7 @@
}
CharSequence requestingAppName = AppLabelProxy.Util.getAppLabel(
- mContext.getPackageManager(), requestingPackageName);
+ mContext, call.getAssociatedUser(), requestingPackageName, mFeatureFlags);
if (requestingAppName == null) {
requestingAppName = requestingPackageName;
}
@@ -3430,35 +3465,42 @@
Log.w(this, "Unknown call (%s) asked to disconnect", call);
} else {
mLocallyDisconnectingCalls.add(call);
- int previousState = call.getState();
- call.disconnect();
- for (CallsManagerListener listener : mListeners) {
- listener.onCallStateChanged(call, previousState, call.getState());
- }
- // Cancel any of the outgoing call futures if they're still around.
- if (mPendingCallConfirm != null && !mPendingCallConfirm.isDone()) {
- mPendingCallConfirm.complete(null);
- mPendingCallConfirm = null;
- }
- if (mPendingAccountSelection != null && !mPendingAccountSelection.isDone()) {
- mPendingAccountSelection.complete(null);
- mPendingAccountSelection = null;
- }
+ mCallSequencingAdapter.disconnectCall(call);
}
}
/**
- * Instructs Telecom to disconnect all calls.
+ * Disconnects the provided call. This is only used when
+ * {@link FeatureFlags#enableCallSequencing()} is false.
+ * @param call The call to disconnect.
+ * @param previousState The previous call state before the call is disconnected.
*/
- void disconnectAllCalls() {
- Log.v(this, "disconnectAllCalls");
-
- for (Call call : mCalls) {
- disconnectCall(call);
- }
+ public void disconnectCallOld(Call call, int previousState) {
+ call.disconnect();
+ processDisconnectCallAndCleanup(call, previousState);
}
/**
+ * Helper to process the call state change upon disconnecting the provided call and performs
+ * local cleanup to clear the outgoing call futures, if they exist.
+ * @param call The call to disconnect.
+ * @param previousState The previous call state before the call is disconnected.
+ */
+ public void processDisconnectCallAndCleanup(Call call, int previousState) {
+ for (CallsManagerListener listener : mListeners) {
+ listener.onCallStateChanged(call, previousState, call.getState());
+ }
+ // Cancel any of the outgoing call futures if they're still around.
+ if (mPendingCallConfirm != null && !mPendingCallConfirm.isDone()) {
+ mPendingCallConfirm.complete(null);
+ mPendingCallConfirm = null;
+ }
+ if (mPendingAccountSelection != null && !mPendingAccountSelection.isDone()) {
+ mPendingAccountSelection.complete(null);
+ mPendingAccountSelection = null;
+ }
+ }
+ /**
* Disconnects calls for any other {@link PhoneAccountHandle} but the one specified.
* Note: As a protective measure, will NEVER disconnect an emergency call. Although that
* situation should never arise, its a good safeguard.
@@ -3549,6 +3591,16 @@
new RequestCallback(new ActionUnHoldCall(call, activeCallId)));
}
+ public void requestActionSetActiveCall(Call call, String tag) {
+ mConnectionSvrFocusMgr.requestFocus(call,
+ new RequestCallback(new ActionSetCallState(call, CallState.ACTIVE, tag)));
+ }
+
+ public void requestFocusActionAnswerCall(Call call, int videoState) {
+ mConnectionSvrFocusMgr.requestFocus(call, new CallsManager.RequestCallback(
+ new ActionAnswerCall(call, videoState)));
+ }
+
@Override
public void onExtrasRemoved(Call c, int source, List<String> keys) {
if (source != Call.SOURCE_CONNECTION_SERVICE) {
@@ -3915,15 +3967,10 @@
maybeMoveToSpeakerPhone(call);
}
- void requestFocusActionAnswerCall(Call call, int videoState) {
- mConnectionSvrFocusMgr.requestFocus(call, new CallsManager.RequestCallback(
- new CallsManager.ActionAnswerCall(call, videoState)));
- }
-
/**
* Returns true if the active call is held.
*/
- boolean holdActiveCallForNewCall(Call call) {
+ public boolean holdActiveCallForNewCall(Call call) {
Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
Log.i(this, "holdActiveCallForNewCall, newCall: %s, activeCall: %s", call.getId(),
(activeCall == null ? "<none>" : activeCall.getId()));
@@ -4009,23 +4056,8 @@
return;
}
- if (holdActiveCallForNewCall(newCall)) {
- // Transactional clients do not call setHold but the request was sent to set the
- // call as inactive and it has already been acked by this point.
- markCallAsOnHold(activeCall);
- callback.onResult(true);
- } else {
- // It's possible that holdActiveCallForNewCall disconnected the activeCall.
- // Therefore, the activeCalls state should be checked before failing.
- if (activeCall.isLocallyDisconnecting()) {
- callback.onResult(true);
- } else {
- Log.i(this, mTag + "active call could not be held or disconnected");
- callback.onError(
- new CallException("activeCall could not be held or disconnected",
- CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
- }
- }
+ mCallSequencingAdapter.transactionHoldPotentialActiveCallForNewCall(newCall,
+ activeCall, callback);
} else {
// before attempting CallsManager#holdActiveCallForNewCall(Call), check if it'll fail
// early
@@ -4053,6 +4085,28 @@
}
}
+ public void transactionHoldPotentialActiveCallForNewCallOld(Call newCall,
+ Call activeCall, OutcomeReceiver<Boolean, CallException> callback) {
+ if (holdActiveCallForNewCall(newCall)) {
+ // Transactional clients do not call setHold but the request was sent to set the
+ // call as inactive and it has already been acked by this point.
+ markCallAsOnHold(activeCall);
+ callback.onResult(true);
+ } else {
+ // It's possible that holdActiveCallForNewCall disconnected the activeCall.
+ // Therefore, the activeCalls state should be checked before failing.
+ if (activeCall.isLocallyDisconnecting()) {
+ callback.onResult(true);
+ } else {
+ Log.i(this, "transactionHoldPotentialActiveCallForNewCallOld: active call could "
+ + "not be held or disconnected");
+ callback.onError(
+ new CallException("activeCall could not be held or disconnected",
+ CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+ }
+ }
+ }
+
private boolean canHoldOrSwapActiveCall(Call activeCall, Call newCall) {
return canHold(activeCall) || sameSourceHoldCase(activeCall, newCall);
}
@@ -4064,8 +4118,6 @@
/**
* CS: Mark a call as active. If the call is self-mangaed, we will also hold any active call
* before moving the self-managed call to active.
- * <p>
- * Note: Only used when {@link FeatureFlags#enableCallSequencing()} is false.
*/
@VisibleForTesting
public void markCallAsActive(Call call) {
@@ -4075,13 +4127,7 @@
// to active directly. We should hold or disconnect the current active call based on the
// holdability, and request the call focus for the self-managed call before the state
// change.
- holdActiveCallForNewCall(call);
- mConnectionSvrFocusMgr.requestFocus(
- call,
- new RequestCallback(new ActionSetCallState(
- call,
- CallState.ACTIVE,
- "active set explicitly for self-managed")));
+ mCallSequencingAdapter.markCallAsActiveSelfManagedCall(call);
} else {
if (mPendingAudioProcessingCall == call) {
if (mCalls.contains(call)) {
@@ -4103,8 +4149,6 @@
/**
* Mark a call as on hold after the hold operation has already completed.
- * <p>
- * Note: only used when {@link FeatureFlags#enableCallSequencing()} is false.
*/
public void markCallAsOnHold(Call call) {
setCallState(call, CallState.ON_HOLD, "on-hold set explicitly");
@@ -4283,17 +4327,19 @@
removeCall(call);
boolean isLocallyDisconnecting = mLocallyDisconnectingCalls.contains(call);
mLocallyDisconnectingCalls.remove(call);
- mCallSequencingAdapter.unholdCallForRemoval(call, isLocallyDisconnecting);
+ maybeMoveHeldCallToForeground(call, isLocallyDisconnecting);
}
/**
* Move the held call to foreground in the event that there is a held call and the disconnected
* call was disconnected locally or the held call has no way to auto-unhold because it does not
* support hold capability.
- * <p>
- * Note: This is only used when {@link FeatureFlags#enableCallSequencing()} is set to false.
+ *
+ * Note: If {@link FeatureFlags#enableCallSequencing()} is enabled, we will verify that the
+ * transaction to unhold the call succeeded or failed.
*/
public void maybeMoveHeldCallToForeground(Call removedCall, boolean isLocallyDisconnecting) {
+ CompletableFuture<Boolean> unholdForegroundCallFuture = null;
Call foregroundCall = mCallAudioManager.getPossiblyHeldForegroundCall();
if (isLocallyDisconnecting) {
boolean isDisconnectingChildCall = removedCall.isDisconnectingChildCall();
@@ -4309,7 +4355,7 @@
&& foregroundCall.getState() == CallState.ON_HOLD
&& areFromSameSource(foregroundCall, removedCall)) {
- foregroundCall.unhold();
+ unholdForegroundCallFuture = foregroundCall.unhold();
}
} else if (foregroundCall != null &&
!foregroundCall.can(Connection.CAPABILITY_SUPPORT_HOLD) &&
@@ -4320,7 +4366,14 @@
// has no means of unholding it themselves.
Log.i(this, "maybeMoveHeldCallToForeground: Auto-unholding held foreground call (call "
+ "doesn't support hold)");
- foregroundCall.unhold();
+ unholdForegroundCallFuture = foregroundCall.unhold();
+ }
+
+ if (mFeatureFlags.enableCallSequencing() && unholdForegroundCallFuture != null) {
+ mCallSequencingAdapter.logFutureResultTransaction(unholdForegroundCallFuture,
+ "maybeMoveHeldCallToForeground", "CM.mMHCTF",
+ "Successfully unheld the foreground call.",
+ "Failed to unhold the foreground call.");
}
}
@@ -4395,7 +4448,7 @@
return getFirstCallWithState(CallState.RINGING, CallState.ANSWERED) != null;
}
- boolean hasRingingOrSimulatedRingingCall() {
+ public boolean hasRingingOrSimulatedRingingCall() {
return getFirstCallWithState(
CallState.SIMULATED_RINGING, CallState.RINGING, CallState.ANSWERED) != null;
}
@@ -5147,7 +5200,7 @@
exceptCall, phoneAccountHandle, ANY_CALL_STATE);
}
- private boolean hasMaximumManagedHoldingCalls(Call exceptCall) {
+ public boolean hasMaximumManagedHoldingCalls(Call exceptCall) {
return MAXIMUM_HOLD_CALLS <= getNumCallsWithState(false /* isSelfManaged */, exceptCall,
null /* phoneAccountHandle */, CallState.ON_HOLD);
}
@@ -5163,7 +5216,7 @@
phoneAccountHandle, CallState.RINGING, CallState.ANSWERED);
}
- private boolean hasMaximumOutgoingCalls(Call exceptCall) {
+ public boolean hasMaximumOutgoingCalls(Call exceptCall) {
return MAXIMUM_LIVE_CALLS <= getNumCallsWithState(CALL_FILTER_ALL,
exceptCall, null /* phoneAccountHandle */, OUTGOING_CALL_STATES);
}
@@ -5280,7 +5333,7 @@
* <p>
* Note: This method is only applicable when {@link FeatureFlags#enableCallSequencing()}
* is false.
- * @param call The new pending outgoing call.
+ * @param emergencyCall The new pending outgoing call.
* @return true if room was made, false if no room could be made.
*/
@VisibleForTesting
@@ -5475,41 +5528,12 @@
return true;
}
- // If the live call is stuck in a connecting state for longer than the transitory timeout,
- // then we should disconnect it in favor of the new outgoing call and prompt the user to
- // generate a bugreport.
- // TODO: In the future we should let the CallAnomalyWatchDog do this disconnection of the
- // live call stuck in the connecting state. Unfortunately that code will get tripped up by
- // calls that have a longer than expected new outgoing call broadcast response time. This
- // mitigation is intended to catch calls stuck in a CONNECTING state for a long time that
- // block outgoing calls. However, if the user dials two calls in quick succession it will
- // result in both calls getting disconnected, which is not optimal.
- if (liveCall.getState() == CallState.CONNECTING
- && ((mClockProxy.elapsedRealtime() - liveCall.getCreationElapsedRealtimeMillis())
- > mTimeoutsAdapter.getNonVoipCallTransitoryStateTimeoutMillis())) {
- if (mFeatureFlags.telecomMetricsSupport()) {
- mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_MANAGER,
- ErrorStats.ERROR_STUCK_CONNECTING);
- }
- mAnomalyReporter.reportAnomaly(LIVE_CALL_STUCK_CONNECTING_ERROR_UUID,
- LIVE_CALL_STUCK_CONNECTING_ERROR_MSG);
- liveCall.disconnect("Force disconnect CONNECTING call.");
- return true;
- }
-
- if (hasMaximumOutgoingCalls(call)) {
- Call outgoingCall = getFirstCallWithState(OUTGOING_CALL_STATES);
- if (outgoingCall.getState() == CallState.SELECT_PHONE_ACCOUNT) {
- // If there is an orphaned call in the {@link CallState#SELECT_PHONE_ACCOUNT}
- // state, just disconnect it since the user has explicitly started a new call.
- call.getAnalytics().setCallIsAdditional(true);
- outgoingCall.getAnalytics().setCallIsInterrupted(true);
- outgoingCall.disconnect("Disconnecting call in SELECT_PHONE_ACCOUNT in favor"
- + " of new outgoing call.");
- return true;
- }
- call.setStartFailCause(CallFailureCause.MAX_OUTGOING_CALLS);
- return false;
+ CompletableFuture<Boolean> disconnectFuture =
+ maybeDisconnectExistingCallForNewOutgoingCall(call, liveCall);
+ // If future is instantiated, it will always be completed when call sequencing
+ // isn't enabled.
+ if (!mFeatureFlags.enableCallSequencing() && disconnectFuture != null) {
+ return disconnectFuture.getNow(false);
}
// TODO: Remove once b/23035408 has been corrected.
@@ -5575,12 +5599,70 @@
}
/**
+ * Potentially disconnects the live call if it has been stuck in a connecting state for more
+ * than the designated timeout or the outgoing call if it's stuck in the
+ * {@link CallState#SELECT_PHONE_ACCOUNT} stage.
+ *
+ * @param call The new outgoing call that is being placed.
+ * @param liveCall The first live call that has been detected.
+ * @return The {@link CompletableFuture<Boolean>} representing if room for the outgoing call
+ * could be made, null if further processing is required.
+ */
+ public CompletableFuture<Boolean> maybeDisconnectExistingCallForNewOutgoingCall(Call call,
+ Call liveCall) {
+ // If the live call is stuck in a connecting state for longer than the transitory timeout,
+ // then we should disconnect it in favor of the new outgoing call and prompt the user to
+ // generate a bugreport.
+ // TODO: In the future we should let the CallAnomalyWatchDog do this disconnection of the
+ // live call stuck in the connecting state. Unfortunately that code will get tripped up by
+ // calls that have a longer than expected new outgoing call broadcast response time. This
+ // mitigation is intended to catch calls stuck in a CONNECTING state for a long time that
+ // block outgoing calls. However, if the user dials two calls in quick succession it will
+ // result in both calls getting disconnected, which is not optimal.
+ if (liveCall.getState() == CallState.CONNECTING
+ && ((mClockProxy.elapsedRealtime() - liveCall.getCreationElapsedRealtimeMillis())
+ > mTimeoutsAdapter.getNonVoipCallTransitoryStateTimeoutMillis())) {
+ if (mFeatureFlags.telecomMetricsSupport()) {
+ mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_MANAGER,
+ ErrorStats.ERROR_STUCK_CONNECTING);
+ }
+ mAnomalyReporter.reportAnomaly(LIVE_CALL_STUCK_CONNECTING_ERROR_UUID,
+ LIVE_CALL_STUCK_CONNECTING_ERROR_MSG);
+ CompletableFuture<Boolean> disconnectFuture =
+ liveCall.disconnect("Force disconnect CONNECTING call.");
+ return mFeatureFlags.enableCallSequencing()
+ ? disconnectFuture
+ : CompletableFuture.completedFuture(true);
+ }
+
+ if (hasMaximumOutgoingCalls(call)) {
+ Call outgoingCall = getFirstCallWithState(OUTGOING_CALL_STATES);
+ if (outgoingCall.getState() == CallState.SELECT_PHONE_ACCOUNT) {
+ // If there is an orphaned call in the {@link CallState#SELECT_PHONE_ACCOUNT}
+ // state, just disconnect it since the user has explicitly started a new call.
+ call.getAnalytics().setCallIsAdditional(true);
+ outgoingCall.getAnalytics().setCallIsInterrupted(true);
+ CompletableFuture<Boolean> disconnectFuture = outgoingCall.disconnect(
+ "Disconnecting call in SELECT_PHONE_ACCOUNT in favor of new "
+ + "outgoing call.");
+ return mFeatureFlags.enableCallSequencing()
+ ? disconnectFuture
+ : CompletableFuture.completedFuture(true);
+ }
+ call.setStartFailCause(CallFailureCause.MAX_OUTGOING_CALLS);
+ return CompletableFuture.completedFuture(false);
+ }
+
+ return null;
+ }
+
+ /**
* Given a call, find the first non-null phone account handle of its children.
*
* @param parentCall The parent call.
* @return The first non-null phone account handle of the children, or {@code null} if none.
*/
- private PhoneAccountHandle getFirstChildPhoneAccount(Call parentCall) {
+ public PhoneAccountHandle getFirstChildPhoneAccount(Call parentCall) {
for (Call childCall : parentCall.getChildCalls()) {
PhoneAccountHandle childPhoneAccount = childCall.getTargetPhoneAccount();
if (childPhoneAccount != null) {
@@ -6662,7 +6744,15 @@
public void performAction() {
synchronized (mLock) {
Log.d(this, "perform unhold call for %s", mCall);
- mCall.unhold("held " + mPreviouslyHeldCallId);
+ CompletableFuture<Boolean> unholdFuture =
+ mCall.unhold("held " + mPreviouslyHeldCallId);
+ if (mFeatureFlags.enableCallSequencing() && unholdFuture != null) {
+ mCallSequencingAdapter.logFutureResultTransaction(unholdFuture,
+ "performAction", "AUC.pA", "performAction: unhold call transaction "
+ + "succeeded. Call state is active.",
+ "performAction: unhold call transaction failed. Call state did not "
+ + "move to active in designated time.");
+ }
}
}
}
@@ -6684,10 +6774,11 @@
listener.onIncomingCallAnswered(mCall);
}
+ CompletableFuture<Boolean> answerCallFuture = null;
// We do not update the UI until we get confirmation of the answer() through
// {@link #markCallAsActive}.
if (mCall.getState() == CallState.RINGING) {
- mCall.answer(mVideoState);
+ answerCallFuture = mCall.answer(mVideoState);
setCallState(mCall, CallState.ANSWERED, "answered");
} else if (mCall.getState() == CallState.SIMULATED_RINGING) {
// If the call's in simulated ringing, we don't have to wait for the CS --
@@ -6698,12 +6789,19 @@
// In certain circumstances, the connection service can lose track of a request
// to answer a call. Therefore, if the user presses answer again, still send it
// on down, but log a warning in the process and don't change the call state.
- mCall.answer(mVideoState);
+ answerCallFuture = mCall.answer(mVideoState);
Log.w(this, "Duplicate answer request for call %s", mCall.getId());
}
if (isSpeakerphoneAutoEnabledForVideoCalls(mVideoState)) {
mCall.setStartWithSpeakerphoneOn(true);
}
+ if (mFeatureFlags.enableCallSequencing() && answerCallFuture != null) {
+ mCallSequencingAdapter.logFutureResultTransaction(answerCallFuture,
+ "performAction", "AAC.pA", "performAction: answer call transaction "
+ + "succeeded. Call state is active.",
+ "performAction: answer call transaction failed. Call state did not "
+ + "move to active in designated time.");
+ }
}
}
}
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 14c8f62..260c238 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -130,11 +130,7 @@
synchronized (mLock) {
logIncoming("handleCreateConnectionComplete %s", callId);
Call call = mCallIdMapper.getCall(callId);
- if (call != null && mScheduledFutureMap.containsKey(call)) {
- ScheduledFuture<?> existingTimeout = mScheduledFutureMap.get(call);
- existingTimeout.cancel(false /* cancelIfRunning */);
- mScheduledFutureMap.remove(call);
- }
+ maybeRemoveCleanupFuture(call);
// Check status hints image for cross user access
if (connection.getStatusHints() != null) {
Icon icon = connection.getStatusHints().getIcon();
@@ -174,11 +170,7 @@
synchronized (mLock) {
logIncoming("handleCreateConferenceComplete %s", callId);
Call call = mCallIdMapper.getCall(callId);
- if (call != null && mScheduledFutureMap.containsKey(call)) {
- ScheduledFuture<?> existingTimeout = mScheduledFutureMap.get(call);
- existingTimeout.cancel(false /* cancelIfRunning */);
- mScheduledFutureMap.remove(call);
- }
+ maybeRemoveCleanupFuture(call);
// Check status hints image for cross user access
if (conference.getStatusHints() != null) {
Icon icon = conference.getStatusHints().getIcon();
@@ -1678,6 +1670,9 @@
Log.getExternalSession(TELECOM_ABBREVIATION));
} catch (RemoteException e) {
Log.e(this, e, "Failure to createConference -- %s", getComponentName());
+ if (mFlags.dontTimeoutDestroyedCalls()) {
+ maybeRemoveCleanupFuture(call);
+ }
mPendingResponses.remove(callId).handleCreateConferenceFailure(
new DisconnectCause(DisconnectCause.ERROR, e.toString()));
}
@@ -1708,6 +1703,9 @@
Log.i(ConnectionServiceWrapper.this, "Call not present"
+ " in call id mapper, maybe it was aborted before the bind"
+ " completed successfully?");
+ if (mFlags.dontTimeoutDestroyedCalls()) {
+ maybeRemoveCleanupFuture(call);
+ }
response.handleCreateConnectionFailure(
new DisconnectCause(DisconnectCause.CANCELED));
return;
@@ -1793,6 +1791,9 @@
mScheduledFutureMap.put(call, future);
try {
if (mFlags.cswServiceInterfaceIsNull() && mServiceInterface == null) {
+ if (mFlags.dontTimeoutDestroyedCalls()) {
+ maybeRemoveCleanupFuture(call);
+ }
mPendingResponses.remove(callId).handleCreateConnectionFailure(
new DisconnectCause(DisconnectCause.ERROR,
"CSW#oCC ServiceInterface is null"));
@@ -1807,6 +1808,9 @@
}
} catch (RemoteException e) {
Log.e(this, e, "Failure to createConnection -- %s", getComponentName());
+ if (mFlags.dontTimeoutDestroyedCalls()) {
+ maybeRemoveCleanupFuture(call);
+ }
mPendingResponses.remove(callId).handleCreateConnectionFailure(
new DisconnectCause(DisconnectCause.ERROR, e.toString()));
}
@@ -2286,6 +2290,9 @@
if (response != null) {
response.handleCreateConnectionFailure(disconnectCause);
}
+ if (mFlags.dontTimeoutDestroyedCalls()) {
+ maybeRemoveCleanupFuture(mCallIdMapper.getCall(callId));
+ }
mCallIdMapper.removeCall(callId);
}
@@ -2295,6 +2302,9 @@
if (response != null) {
response.handleCreateConnectionFailure(disconnectCause);
}
+ if (mFlags.dontTimeoutDestroyedCalls()) {
+ maybeRemoveCleanupFuture(call);
+ }
mCallIdMapper.removeCall(call);
}
@@ -2754,4 +2764,20 @@
public void setAnomalyReporterAdapter(AnomalyReporterAdapter mAnomalyReporterAdapter){
mAnomalyReporter = mAnomalyReporterAdapter;
}
+
+ /**
+ * Given a call, unschedule and cancel the cleanup future.
+ * @param call the call.
+ */
+ private void maybeRemoveCleanupFuture(Call call) {
+ if (call == null) {
+ return;
+ }
+ ScheduledFuture<?> future = mScheduledFutureMap.remove(call);
+ if (future == null) {
+ return;
+ }
+ future.cancel(false /* interrupt */);
+
+ }
}
diff --git a/src/com/android/server/telecom/PendingAudioRoute.java b/src/com/android/server/telecom/PendingAudioRoute.java
index d21ac56..dde1d8d 100644
--- a/src/com/android/server/telecom/PendingAudioRoute.java
+++ b/src/com/android/server/telecom/PendingAudioRoute.java
@@ -70,8 +70,23 @@
mCommunicationDeviceType = AudioRoute.TYPE_INVALID;
}
- void setOrigRoute(boolean active, AudioRoute origRoute) {
- origRoute.onOrigRouteAsPendingRoute(active, this, mAudioManager, mBluetoothRouteManager);
+ /**
+ * Sets the originating route information, and begins the process of transitioning OUT of the
+ * originating route.
+ * Note: We also pass in whether the destination route is going to be active. This is so that
+ * {@link AudioRoute#onOrigRouteAsPendingRoute(boolean, PendingAudioRoute, AudioManager,
+ * BluetoothRouteManager)} knows whether or not the destination route will be active or not and
+ * can determine whether or not it needs to call {@link AudioManager#clearCommunicationDevice()}
+ * or not. To optimize audio performance we only need to clear the communication device if the
+ * end result is going to be that we are in an inactive state.
+ * @param isOriginActive Whether the origin is active.
+ * @param origRoute The origin.
+ * @param isDestActive Whether the destination will be active.
+ */
+ void setOrigRoute(boolean isOriginActive, AudioRoute origRoute, boolean isDestActive) {
+ mActive = isDestActive;
+ origRoute.onOrigRouteAsPendingRoute(isOriginActive, this, mAudioManager,
+ mBluetoothRouteManager);
mOrigRoute = origRoute;
}
@@ -134,6 +149,10 @@
return mPendingMessages;
}
+ /**
+ * Whether the destination {@link #getDestRoute()} will be active or not.
+ * @return {@code true} if destination will be active, {@code false} otherwise.
+ */
public boolean isActive() {
return mActive;
}
@@ -154,4 +173,14 @@
public FeatureFlags getFeatureFlags() {
return mFeatureFlags;
}
+
+ @Override
+ public String toString() {
+ return "PendingAudioRoute{" +
+ ", mOrigRoute=" + mOrigRoute +
+ ", mDestRoute=" + mDestRoute +
+ ", mActive=" + mActive +
+ ", mCommunicationDeviceType=" + mCommunicationDeviceType +
+ '}';
+ }
}
diff --git a/src/com/android/server/telecom/PhoneAccountRegistrar.java b/src/com/android/server/telecom/PhoneAccountRegistrar.java
index 796a62b..1a1af92 100644
--- a/src/com/android/server/telecom/PhoneAccountRegistrar.java
+++ b/src/com/android/server/telecom/PhoneAccountRegistrar.java
@@ -1326,7 +1326,10 @@
// Ensure name is correct.
CharSequence newLabel = mAppLabelProxy.getAppLabel(
- account.getAccountHandle().getComponentName().getPackageName());
+ account.getAccountHandle().getComponentName().getPackageName(),
+ UserUtil.getAssociatedUserForCall(
+ mTelecomFeatureFlags.associatedUserRefactorForWorkProfile(),
+ this, UserHandle.CURRENT, account.getAccountHandle()));
account = account.toBuilder()
.setLabel(newLabel)
diff --git a/src/com/android/server/telecom/Ringer.java b/src/com/android/server/telecom/Ringer.java
index 12778b0..bfaadf0 100644
--- a/src/com/android/server/telecom/Ringer.java
+++ b/src/com/android/server/telecom/Ringer.java
@@ -59,6 +59,7 @@
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
@@ -176,6 +177,11 @@
private static VolumeShaper.Configuration mVolumeShaperConfig;
+ public static final UUID GET_RINGER_MODE_ANOMALY_UUID =
+ UUID.fromString("eb10505b-4d7b-4fab-b4a1-a18186799065");
+ public static final String GET_RINGER_MODE_ANOMALY_MSG = "AM#GetRingerMode() and"
+ + " AM#GetRingerModeInternal() are returning diff values when DoNotDisturb is OFF!";
+
/**
* Used to keep ordering of unanswered incoming calls. There can easily exist multiple incoming
* calls and explicit ordering is useful for maintaining the proper state of the ringer.
@@ -191,6 +197,8 @@
private final boolean mIsHapticPlaybackSupportedByDevice;
private final FeatureFlags mFlags;
private final boolean mRingtoneVibrationSupported;
+ private final AnomalyReporterAdapter mAnomalyReporter;
+
/**
* For unit testing purposes only; when set, {@link #startRinging(Call, boolean)} will complete
* the future provided by the test using {@link #setBlockOnRingingFuture(CompletableFuture)}.
@@ -237,7 +245,8 @@
InCallController inCallController,
NotificationManager notificationManager,
AccessibilityManagerAdapter accessibilityManagerAdapter,
- FeatureFlags featureFlags) {
+ FeatureFlags featureFlags,
+ AnomalyReporterAdapter anomalyReporter) {
mLock = new Object();
mSystemSettingsUtil = systemSettingsUtil;
@@ -252,6 +261,7 @@
mVibrationEffectProxy = vibrationEffectProxy;
mNotificationManager = notificationManager;
mAccessibilityManagerAdapter = accessibilityManagerAdapter;
+ mAnomalyReporter = anomalyReporter;
mDefaultVibrationEffect =
loadDefaultRingVibrationEffect(
@@ -405,10 +415,9 @@
// If ringer is not audible for this call, then the phone is in "Vibrate" mode.
// Use haptic-only ringtone or do not play anything.
isHapticOnly = true;
- if (DEBUG_RINGER) {
- Log.i(this, "Set ringtone as haptic only: " + isHapticOnly);
- }
+ Log.i(this, "Set ringtone as haptic only: " + isHapticOnly);
} else {
+ Log.i(this, "ringer & haptics are off, user missed alerts for call");
foregroundCall.setUserMissed(USER_MISSED_NO_VIBRATE);
Log.addEvent(foregroundCall, LogUtils.Events.SKIP_VIBRATION,
vibratorAttrs);
@@ -437,7 +446,7 @@
ringtoneInfoSupplier = () -> mRingtoneFactory.getRingtone(
foregroundCall, null, false);
}
-
+ Log.i(this, "isRingtoneInfoSupplierNull=[%b]", ringtoneInfoSupplier == null);
// If vibration will be done, reserve the vibrator.
boolean vibratorReserved = isVibratorEnabled && attributes.shouldRingForContact()
&& tryReserveVibration(foregroundCall);
@@ -706,12 +715,43 @@
// AudioManager#getRingerModeInternal which only useful for volume controllers
boolean zenModeOn = mNotificationManager != null
&& mNotificationManager.getZenMode() != ZEN_MODE_OFF;
+ maybeGenAnomReportForGetRingerMode(zenModeOn, audioManager);
return mVibrator.hasVibrator()
&& mSystemSettingsUtil.isRingVibrationEnabled(context)
&& (audioManager.getRingerMode() != AudioManager.RINGER_MODE_SILENT
|| (zenModeOn && shouldRingForContact));
}
+ /**
+ * There are 3 settings for haptics:
+ * - AudioManager.RINGER_MODE_SILENT
+ * - AudioManager.RINGER_MODE_VIBRATE
+ * - AudioManager.RINGER_MODE_NORMAL
+ * If the user does not have {@link AudioManager#RINGER_MODE_SILENT} set, the user should
+ * have haptic feeback
+ *
+ * Note: If DND/ZEN_MODE is on, {@link AudioManager#getRingerMode()} will return
+ * {@link AudioManager#RINGER_MODE_SILENT}, regardless of the user setting. Therefore,
+ * getRingerModeInternal is the source of truth instead of {@link AudioManager#getRingerMode()}.
+ * However, if DND/ZEN_MOD is off, the APIs should return the same value. Generate an anomaly
+ * report if they diverge.
+ */
+ private void maybeGenAnomReportForGetRingerMode(boolean isZenModeOn, AudioManager am) {
+ if (!mFlags.getRingerModeAnomReport()) {
+ return;
+ }
+ if (!isZenModeOn) {
+ int ringerMode = am.getRingerMode();
+ int ringerModeInternal = am.getRingerModeInternal();
+ if (ringerMode != ringerModeInternal) {
+ Log.i(this, "getRingerMode=[%d], getRingerModeInternal=[%d]",
+ ringerMode, ringerModeInternal);
+ mAnomalyReporter.reportAnomaly(GET_RINGER_MODE_ANOMALY_UUID,
+ GET_RINGER_MODE_ANOMALY_MSG);
+ }
+ }
+ }
+
private RingerAttributes getRingerAttributes(Call call, boolean isHfpDeviceAttached) {
mAudioManager = mContext.getSystemService(AudioManager.class);
RingerAttributes.Builder builder = new RingerAttributes.Builder();
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index a662dde..88adf14 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -52,6 +52,8 @@
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
import android.os.OutcomeReceiver;
import android.os.ParcelFileDescriptor;
import android.os.Process;
@@ -81,6 +83,7 @@
import com.android.internal.telecom.ICallEventCallback;
import com.android.internal.telecom.ITelecomService;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.callsequencing.voip.OutgoingCallTransactionSequencing;
import com.android.server.telecom.components.UserCallIntentProcessorFactory;
import com.android.server.telecom.flags.FeatureFlags;
import com.android.server.telecom.metrics.ApiStats;
@@ -102,6 +105,7 @@
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
// TODO: Needed for move to system service: import com.android.internal.R;
@@ -160,6 +164,7 @@
private final FeatureFlags mFeatureFlags;
private final com.android.internal.telephony.flags.FeatureFlags mTelephonyFeatureFlags;
private final TelecomMetricsController mMetricsController;
+ private final String mSystemUiPackageName;
private AnomalyReporterAdapter mAnomalyReporter = new AnomalyReporterAdapterImpl();
private final Context mContext;
private final AppOpsManager mAppOpsManager;
@@ -192,65 +197,64 @@
extras.putInt(CallAttributes.CALLER_UID_KEY, Binder.getCallingUid());
extras.putInt(CallAttributes.CALLER_PID_KEY, Binder.getCallingPid());
- CallTransaction transaction = null;
- // create transaction based on the call direction
- switch (callAttributes.getDirection()) {
- case DIRECTION_OUTGOING:
- transaction = new OutgoingCallTransaction(callId, mContext, callAttributes,
- mCallsManager, extras, mFeatureFlags);
- break;
- case DIRECTION_INCOMING:
- transaction = new IncomingCallTransaction(callId, callAttributes,
- mCallsManager, extras, mFeatureFlags);
- break;
- default:
- throw new IllegalArgumentException(String.format("Invalid Call Direction. "
- + "Was [%d] but should be within [%d,%d]",
- callAttributes.getDirection(), DIRECTION_INCOMING,
- DIRECTION_OUTGOING));
+
+ CompletableFuture<CallTransaction> transactionFuture;
+ long token = Binder.clearCallingIdentity();
+ try {
+ transactionFuture = mCallsManager.createTransactionalCall(callId,
+ callAttributes, extras, callingPackage);
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
- mTransactionManager.addTransaction(transaction, new OutcomeReceiver<>() {
- @Override
- public void onResult(CallTransactionResult result) {
- Log.d(TAG, "addCall: onResult");
- Call call = result.getCall();
+ transactionFuture.thenCompose((transaction) -> {
+ if (transaction != null) {
+ mTransactionManager.addTransaction(transaction, new OutcomeReceiver<>() {
+ @Override
+ public void onResult(CallTransactionResult result) {
+ Log.d(TAG, "addCall: onResult");
+ Call call = result.getCall();
- if (call == null || !call.getId().equals(callId)) {
- Log.i(TAG, "addCall: onResult: call is null or id mismatch");
- onAddCallControl(callId, callEventCallback, null,
- new CallException(ADD_CALL_ERR_MSG, CODE_ERROR_UNKNOWN));
- return;
- }
+ if (call == null || !call.getId().equals(callId)) {
+ Log.i(TAG, "addCall: onResult: call is null or id mismatch");
+ onAddCallControl(callId, callEventCallback, null,
+ new CallException(ADD_CALL_ERR_MSG,
+ CODE_ERROR_UNKNOWN));
+ return;
+ }
- TransactionalServiceWrapper serviceWrapper =
- mTransactionalServiceRepository
- .addNewCallForTransactionalServiceWrapper(handle,
- callEventCallback, mCallsManager, call);
+ TransactionalServiceWrapper serviceWrapper =
+ mTransactionalServiceRepository
+ .addNewCallForTransactionalServiceWrapper(handle,
+ callEventCallback, mCallsManager, call);
- call.setTransactionServiceWrapper(serviceWrapper);
+ call.setTransactionServiceWrapper(serviceWrapper);
- if (mFeatureFlags.transactionalVideoState()) {
- call.setTransactionalCallSupportsVideoCalling(callAttributes);
- }
- ICallControl clientCallControl = serviceWrapper.getICallControl();
+ if (mFeatureFlags.transactionalVideoState()) {
+ call.setTransactionalCallSupportsVideoCalling(callAttributes);
+ }
+ ICallControl clientCallControl = serviceWrapper.getICallControl();
- if (clientCallControl == null) {
- throw new IllegalStateException("TransactionalServiceWrapper"
- + "#ICallControl is null.");
- }
+ if (clientCallControl == null) {
+ throw new IllegalStateException("TransactionalServiceWrapper"
+ + "#ICallControl is null.");
+ }
- // finally, send objects back to the client
- onAddCallControl(callId, callEventCallback, clientCallControl, null);
+ // finally, send objects back to the client
+ onAddCallControl(callId, callEventCallback, clientCallControl,
+ null);
+ }
+
+ @Override
+ public void onError(@NonNull CallException exception) {
+ Log.d(TAG, "addCall: onError: e=[%s]", exception.toString());
+ onAddCallControl(callId, callEventCallback, null, exception);
+ }
+ });
}
-
- @Override
- public void onError(@NonNull CallException exception) {
- Log.d(TAG, "addCall: onError: e=[%s]", exception.toString());
- onAddCallControl(callId, callEventCallback, null, exception);
- }
+ event.setResult(ApiStats.RESULT_NORMAL);
+ return CompletableFuture.completedFuture(transaction);
});
- event.setResult(ApiStats.RESULT_NORMAL);
} finally {
logEvent(event);
Log.endSession();
@@ -1437,7 +1441,7 @@
// ensure the callingPackage is not spoofed
// skip check for privileged UIDs and throw SE if package does not match records
- if (!isPrivilegedUid(callingPackage)
+ if (!isPrivilegedUid()
&& !callingUidMatchesPackageManagerRecords(callingPackage)) {
EventLog.writeEvent(0x534e4554, "236813210", Binder.getCallingUid(),
"getCallStateUsingPackage");
@@ -1470,17 +1474,36 @@
}
}
- private boolean isPrivilegedUid(String callingPackage) {
+ private boolean isPrivilegedUid() {
int callingUid = Binder.getCallingUid();
- boolean isPrivileged = false;
- switch (callingUid) {
- case Process.ROOT_UID:
- case Process.SYSTEM_UID:
- case Process.SHELL_UID:
- isPrivileged = true;
- break;
+ return mFeatureFlags.allowSystemAppsResolveVoipCalls()
+ ? (UserHandle.isSameApp(callingUid, Process.ROOT_UID)
+ || UserHandle.isSameApp(callingUid, Process.SYSTEM_UID)
+ || UserHandle.isSameApp(callingUid, Process.SHELL_UID))
+ : (callingUid == Process.ROOT_UID
+ || callingUid == Process.SYSTEM_UID
+ || callingUid == Process.SHELL_UID);
+ }
+
+ private boolean isSysUiUid() {
+ int callingUid = Binder.getCallingUid();
+ int systemUiUid;
+ if (mPackageManager != null && mSystemUiPackageName != null) {
+ try {
+ systemUiUid = mPackageManager.getPackageUid(mSystemUiPackageName, 0);
+ Log.i(TAG, "isSysUiUid: callingUid = " + callingUid + "; systemUiUid = "
+ + systemUiUid);
+ return UserHandle.isSameApp(callingUid, systemUiUid);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(TAG, "isSysUiUid: caught PackageManager NameNotFoundException = " + e);
+ return false;
+ }
+ } else {
+ Log.w(TAG, "isSysUiUid: caught null check and returned false; "
+ + "mPackageManager = " + mPackageManager + "; mSystemUiPackageName = "
+ + mSystemUiPackageName);
}
- return isPrivileged;
+ return false;
}
/**
@@ -1496,11 +1519,18 @@
if (!enforceAnswerCallPermission(callingPackage, Binder.getCallingUid())) {
throw new SecurityException("requires ANSWER_PHONE_CALLS permission");
}
-
+ // Legacy behavior is to ignore whether the invocation is from a system app:
+ boolean isCallerPrivileged = false;
+ if (mFeatureFlags.allowSystemAppsResolveVoipCalls()) {
+ isCallerPrivileged = isPrivilegedUid() || isSysUiUid();
+ Log.i(TAG, "endCall: Binder.getCallingUid = [" +
+ Binder.getCallingUid() + "] isCallerPrivileged = " +
+ isCallerPrivileged);
+ }
long token = Binder.clearCallingIdentity();
event.setResult(ApiStats.RESULT_NORMAL);
try {
- return endCallInternal(callingPackage);
+ return endCallInternal(callingPackage, isCallerPrivileged);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -1522,11 +1552,19 @@
Log.startSession("TSI.aRC", Log.getPackageAbbreviation(packageName));
synchronized (mLock) {
if (!enforceAnswerCallPermission(packageName, Binder.getCallingUid())) return;
-
+ // Legacy behavior is to ignore whether the invocation is from a system app:
+ boolean isCallerPrivileged = false;
+ if (mFeatureFlags.allowSystemAppsResolveVoipCalls()) {
+ isCallerPrivileged = isPrivilegedUid() || isSysUiUid();
+ Log.i(TAG, "acceptRingingCall: Binder.getCallingUid = [" +
+ Binder.getCallingUid() + "] isCallerPrivileged = " +
+ isCallerPrivileged);
+ }
long token = Binder.clearCallingIdentity();
event.setResult(ApiStats.RESULT_NORMAL);
try {
- acceptRingingCallInternal(DEFAULT_VIDEO_STATE, packageName);
+ acceptRingingCallInternal(DEFAULT_VIDEO_STATE, packageName,
+ isCallerPrivileged);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -1549,11 +1587,18 @@
Log.startSession("TSI.aRCWVS", Log.getPackageAbbreviation(packageName));
synchronized (mLock) {
if (!enforceAnswerCallPermission(packageName, Binder.getCallingUid())) return;
-
+ // Legacy behavior is to ignore whether the invocation is from a system app:
+ boolean isCallerPrivileged = false;
+ if (mFeatureFlags.allowSystemAppsResolveVoipCalls()) {
+ isCallerPrivileged = isPrivilegedUid() || isSysUiUid();
+ Log.i(TAG, "acceptRingingCallWithVideoState: Binder.getCallingUid = "
+ + "[" + Binder.getCallingUid() + "] isCallerPrivileged = " +
+ isCallerPrivileged);
+ }
long token = Binder.clearCallingIdentity();
event.setResult(ApiStats.RESULT_NORMAL);
try {
- acceptRingingCallInternal(videoState, packageName);
+ acceptRingingCallInternal(videoState, packageName, isCallerPrivileged);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -2918,7 +2963,8 @@
SettingsSecureAdapter settingsSecureAdapter,
FeatureFlags featureFlags,
com.android.internal.telephony.flags.FeatureFlags telephonyFeatureFlags,
- TelecomSystem.SyncRoot lock, TelecomMetricsController metricsController) {
+ TelecomSystem.SyncRoot lock, TelecomMetricsController metricsController,
+ String sysUiPackageName) {
mContext = context;
mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
@@ -2940,6 +2986,7 @@
mSubscriptionManagerAdapter = subscriptionManagerAdapter;
mSettingsSecureAdapter = settingsSecureAdapter;
mMetricsController = metricsController;
+ mSystemUiPackageName = sysUiPackageName;
mDefaultDialerCache.observeDefaultDialerApplication(mContext.getMainExecutor(), userId -> {
String defaultDialer = mDefaultDialerCache.getDefaultDialerApplication(userId);
@@ -3054,13 +3101,14 @@
return false;
}
- private void acceptRingingCallInternal(int videoState, String packageName) {
+ private void acceptRingingCallInternal(int videoState, String packageName,
+ boolean isCallerPrivileged) {
Call call = mCallsManager.getFirstCallWithState(CallState.RINGING,
CallState.SIMULATED_RINGING);
if (call != null) {
- if (call.isSelfManaged()) {
+ if (call.isSelfManaged() && !isCallerPrivileged) {
Log.addEvent(call, LogUtils.Events.REQUEST_ACCEPT,
- "self-mgd accept ignored from " + packageName);
+ "self-mgd accept ignored from non-privileged app " + packageName);
return;
}
@@ -3075,7 +3123,7 @@
// Supporting methods for the ITelecomService interface implementation.
//
- private boolean endCallInternal(String callingPackage) {
+ private boolean endCallInternal(String callingPackage, boolean isCallerPrivileged) {
// Always operate on the foreground call if one exists, otherwise get the first call in
// priority order by call-state.
Call call = mCallsManager.getForegroundCall();
@@ -3095,9 +3143,10 @@
return false;
}
- if (call.isSelfManaged()) {
+ if (call.isSelfManaged() && !isCallerPrivileged) {
Log.addEvent(call, LogUtils.Events.REQUEST_DISCONNECT,
- "self-mgd disconnect ignored from " + callingPackage);
+ "self-mgd disconnect ignored from non-privileged app " +
+ callingPackage);
return false;
}
@@ -3600,10 +3649,11 @@
// Note: Important to clear the calling identity since the code below calls into RoleManager
// to check who holds the dialer role, and that requires MANAGE_ROLE_HOLDERS permission
// which is a system permission.
+ int callingUserId = Binder.getCallingUserHandle().getIdentifier();
long token = Binder.clearCallingIdentity();
try {
return mDefaultDialerCache.isDefaultOrSystemDialer(
- callingPackage, Binder.getCallingUserHandle().getIdentifier());
+ callingPackage, callingUserId);
} finally {
Binder.restoreCallingIdentity(token);
}
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 94bea42..7020885 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -224,6 +224,7 @@
RoleManagerAdapter roleManagerAdapter,
ContactsAsyncHelper.Factory contactsAsyncHelperFactory,
DeviceIdleControllerAdapter deviceIdleControllerAdapter,
+ String sysUiPackageName,
Ringer.AccessibilityManagerAdapter accessibilityManagerAdapter,
Executor asyncTaskExecutor,
Executor asyncCallAudioTaskExecutor,
@@ -245,8 +246,8 @@
// Wrap this in a try block to ensure session cleanup occurs in the case of error.
try {
mPhoneAccountRegistrar = new PhoneAccountRegistrar(mContext, mLock, defaultDialerCache,
- packageName -> AppLabelProxy.Util.getAppLabel(
- mContext.getPackageManager(), packageName), null, mFeatureFlags);
+ (packageName, userHandle) -> AppLabelProxy.Util.getAppLabel(mContext,
+ userHandle, packageName, mFeatureFlags), null, mFeatureFlags);
mContactsAsyncHelper = contactsAsyncHelperFactory.create(
new ContactsAsyncHelper.ContentResolverAdapter() {
@@ -386,8 +387,8 @@
CallStreamingNotification callStreamingNotification =
new CallStreamingNotification(mContext,
- packageName -> AppLabelProxy.Util.getAppLabel(
- mContext.getPackageManager(), packageName), asyncTaskExecutor);
+ (packageName, userHandle) -> AppLabelProxy.Util.getAppLabel(mContext,
+ userHandle, packageName, mFeatureFlags), asyncTaskExecutor);
mCallsManager = new CallsManager(
mContext,
@@ -503,7 +504,8 @@
featureFlags,
null,
mLock,
- metricsController);
+ metricsController,
+ sysUiPackageName);
} finally {
Log.endSession();
}
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
index cf5ef41..d63a0bd 100644
--- a/src/com/android/server/telecom/TransactionalServiceWrapper.java
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -401,11 +401,12 @@
return onSetActiveFuture;
}
- public void onAnswer(Call call, int videoState) {
+ public CompletableFuture<Boolean> onAnswer(Call call, int videoState) {
+ CompletableFuture<Boolean> onAnswerFuture;
try {
Log.startSession("TSW.oA");
Log.d(TAG, String.format(Locale.US, "onAnswer: callId=[%s]", call.getId()));
- mCallSequencingAdapter.onSetAnswered(call, videoState,
+ onAnswerFuture = mCallSequencingAdapter.onSetAnswered(call, videoState,
new CallEventCallbackAckTransaction(mICallEventCallback,
ON_ANSWER, call.getId(), videoState, mLock),
result -> Log.i(TAG, String.format(Locale.US,
@@ -414,6 +415,7 @@
} finally {
Log.endSession();
}
+ return onAnswerFuture;
}
public CompletableFuture<Boolean> onSetInactive(Call call) {
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
index 176e479..550a815 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
@@ -34,6 +34,7 @@
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.os.Bundle;
+import android.telecom.CallAudioState;
import android.telecom.Log;
import android.util.ArraySet;
import android.util.LocalLog;
@@ -233,7 +234,8 @@
}
};
- private void handleAudioRefactoringServiceDisconnected(int profile) {
+ @VisibleForTesting
+ public void handleAudioRefactoringServiceDisconnected(int profile) {
CallAudioRouteController controller = (CallAudioRouteController)
mCallAudioRouteAdapter;
Map<AudioRoute, BluetoothDevice> btRoutes = controller
@@ -257,8 +259,23 @@
mCallAudioRouteAdapter.sendMessageWithSessionInfo(
BT_DEVICE_REMOVED, route.getType(), device);
}
- mCallAudioRouteAdapter.sendMessageWithSessionInfo(
- SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE, (String) null);
+
+ if (mFeatureFlags.skipBaselineSwitchWhenRouteNotBluetooth()) {
+ CallAudioState currentAudioState = controller.getCurrentCallAudioState();
+ int currentRoute = currentAudioState.getRoute();
+ if (currentRoute == CallAudioState.ROUTE_BLUETOOTH) {
+ Log.d(this, "handleAudioRefactoringServiceDisconnected: call audio "
+ + "is currently routed to BT so switching back to baseline");
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+ SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE, (String) null);
+ } else {
+ Log.d(this, "handleAudioRefactoringServiceDisconnected: call audio "
+ + "is not currently routed to BT so skipping switch to baseline");
+ }
+ } else {
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+ SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE, (String) null);
+ }
}
private final LinkedHashMap<String, BluetoothDevice> mHfpDevicesByAddress =
@@ -308,19 +325,19 @@
mFeatureFlags = featureFlags;
if (bluetoothAdapter != null) {
mBluetoothAdapter = bluetoothAdapter;
- if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
- mBluetoothHeadsetFuture = new CompletableFuture<>();
- }
bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
BluetoothProfile.HEADSET);
bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
BluetoothProfile.HEARING_AID);
bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
BluetoothProfile.LE_AUDIO);
- mAudioManager = context.getSystemService(AudioManager.class);
- mExecutor = context.getMainExecutor();
- mCommunicationDeviceTracker = communicationDeviceTracker;
}
+ if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ mBluetoothHeadsetFuture = new CompletableFuture<>();
+ }
+ mAudioManager = context.getSystemService(AudioManager.class);
+ mExecutor = context.getMainExecutor();
+ mCommunicationDeviceTracker = communicationDeviceTracker;
}
public void setBluetoothRouteManager(BluetoothRouteManager brm) {
@@ -519,7 +536,10 @@
Log.i(this, "onDeviceConnected: Adding device with address: %s and devicetype=%s",
device, getDeviceTypeString(deviceType));
targetDeviceMap.put(device.getAddress(), device);
- mBluetoothRouteManager.onDeviceAdded(device.getAddress());
+ if (!mFeatureFlags.keepBluetoothDevicesCacheUpdated()
+ || !mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ mBluetoothRouteManager.onDeviceAdded(device.getAddress());
+ }
}
}
}
@@ -551,7 +571,10 @@
Log.i(this, "onDeviceDisconnected: Removing device with address: %s, devicetype=%s",
device, getDeviceTypeString(deviceType));
targetDeviceMap.remove(device.getAddress());
- mBluetoothRouteManager.onDeviceLost(device.getAddress());
+ if (!mFeatureFlags.keepBluetoothDevicesCacheUpdated()
+ || !mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ mBluetoothRouteManager.onDeviceLost(device.getAddress());
+ }
}
}
}
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
index 7667ebc..1cea531 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
@@ -211,6 +211,9 @@
if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_DEVICE_ADDED,
audioRouteType, device);
+ if (mFeatureFlags.keepBluetoothDevicesCacheUpdated()) {
+ mBluetoothDeviceManager.onDeviceConnected(device, deviceType);
+ }
} else {
mBluetoothDeviceManager.onDeviceConnected(device, deviceType);
}
@@ -219,6 +222,9 @@
if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_DEVICE_REMOVED,
audioRouteType, device);
+ if (mFeatureFlags.keepBluetoothDevicesCacheUpdated()) {
+ mBluetoothDeviceManager.onDeviceDisconnected(device, deviceType);
+ }
} else {
mBluetoothDeviceManager.onDeviceDisconnected(device, deviceType);
}
@@ -252,17 +258,14 @@
CallAudioRouteController audioRouteController = (CallAudioRouteController)
mCallAudioRouteAdapter;
if (device == null) {
- if (!mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()) {
- audioRouteController.updateActiveBluetoothDevice(
- new Pair(audioRouteType, null));
- }
+ // Update the active device cache immediately.
+ audioRouteController.updateActiveBluetoothDevice(new Pair(audioRouteType, null));
mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_GONE,
audioRouteType);
} else {
- if (!mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()) {
- audioRouteController.updateActiveBluetoothDevice(
- new Pair(audioRouteType, device.getAddress()));
- }
+ // Update the active device cache immediately.
+ audioRouteController.updateActiveBluetoothDevice(
+ new Pair(audioRouteType, device.getAddress()));
mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
audioRouteType, device.getAddress());
if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID
diff --git a/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java b/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java
index f07c0aa..efac87d 100644
--- a/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java
+++ b/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java
@@ -269,7 +269,8 @@
mContext = context;
mPackageManager = mContext.getPackageManager();
mCallsManager = callsManager;
- mAppName = appLabelProxy.getAppLabel(mPackageName);
+ mAppName = appLabelProxy.getAppLabel(mPackageName,
+ mCall.getAssociatedUser());
mParcelableCallUtilsConverter = parcelableCallUtilsConverter;
}
diff --git a/src/com/android/server/telecom/callsequencing/CallSequencingController.java b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
index 2f0ae45..2794496 100644
--- a/src/com/android/server/telecom/callsequencing/CallSequencingController.java
+++ b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
@@ -16,67 +16,813 @@
package com.android.server.telecom.callsequencing;
+import static android.Manifest.permission.CALL_PRIVILEGED;
+import static android.telecom.CallException.CODE_ERROR_UNKNOWN;
+
+import static com.android.server.telecom.CallsManager.LIVE_CALL_STUCK_CONNECTING_EMERGENCY_ERROR_MSG;
+import static com.android.server.telecom.CallsManager.LIVE_CALL_STUCK_CONNECTING_EMERGENCY_ERROR_UUID;
+import static com.android.server.telecom.CallsManager.OUTGOING_CALL_STATES;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
+import android.os.OutcomeReceiver;
+import android.telecom.CallAttributes;
+import android.telecom.CallException;
+import android.telecom.DisconnectCause;
+import android.telecom.Log;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.AnomalyReporter;
+import androidx.annotation.NonNull;
+
+import com.android.internal.telecom.ICallControl;
import com.android.server.telecom.Call;
+import com.android.server.telecom.CallState;
import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.LogUtils;
+import com.android.server.telecom.LoggedHandlerExecutor;
+import com.android.server.telecom.TransactionalServiceWrapper;
+import com.android.server.telecom.callsequencing.voip.OutgoingCallTransaction;
+import com.android.server.telecom.callsequencing.voip.OutgoingCallTransactionSequencing;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.stats.CallFailureCause;
import java.util.concurrent.CompletableFuture;
/**
* Controls the sequencing between calls when moving between the user ACTIVE (RINGING/ACTIVE) and
- * user INACTIVE (INCOMING/HOLD/DISCONNECTED) states.
+ * user INACTIVE (INCOMING/HOLD/DISCONNECTED) states. This controller is gated by the
+ * {@link FeatureFlags#enableCallSequencing()} flag. Call state changes are verified on a
+ * transactional basis where each operation is verified step by step for cross-phone account calls
+ * or just for the focus call in the case of processing calls on the same phone account.
*/
public class CallSequencingController {
-// private final CallsManager mCallsManager;
+ private final CallsManager mCallsManager;
+ private final Handler mHandler;
+ private final Context mContext;
+ private final FeatureFlags mFeatureFlags;
private final TransactionManager mTransactionManager;
-// private final Handler mHandler;
-// private boolean mCallSequencingEnabled;
+ private boolean mProcessingCallSequencing;
- public CallSequencingController(CallsManager callsManager, boolean callSequencingEnabled) {
-// mCallsManager = callsManager;
- mTransactionManager = TransactionManager.getInstance();
+ public CallSequencingController(CallsManager callsManager, Context context,
+ FeatureFlags featureFlags) {
+ mCallsManager = callsManager;
HandlerThread handlerThread = new HandlerThread(this.toString());
handlerThread.start();
-// mHandler = new Handler(handlerThread.getLooper());
-// mCallSequencingEnabled = callSequencingEnabled;
+ mHandler = new Handler(handlerThread.getLooper());
+ mProcessingCallSequencing = false;
+ mFeatureFlags = featureFlags;
+ mContext = context;
+ mTransactionManager = TransactionManager.getInstance();;
}
+ /**
+ * Creates the outgoing call transaction given that call sequencing is enabled. Two separate
+ * transactions are being tracked here; one is if room needs to be made for the outgoing call
+ * and another to verify that the new call was placed. We need to ensure that the transaction
+ * to make room for the outgoing call is processed beforehand (i.e. see
+ * {@link OutgoingCallTransaction}.
+ * @param callAttributes The call attributes associated with the call.
+ * @param extras The extras that are associated with the call.
+ * @param callingPackage The calling package representing where the request was invoked from.
+ * @return The {@link CompletableFuture<CallTransaction>} that encompasses the request to
+ * place/receive the transactional call.
+ */
+ public CompletableFuture<CallTransaction> createTransactionalOutgoingCall(String callId,
+ CallAttributes callAttributes, Bundle extras, String callingPackage) {
+ PhoneAccountHandle requestedAccountHandle = callAttributes.getPhoneAccountHandle();
+ Uri address = callAttributes.getAddress();
+ if (mCallsManager.isOutgoingCallPermitted(requestedAccountHandle)) {
+ Log.d(this, "createTransactionalOutgoingCall: outgoing call permitted");
+ final boolean hasCallPrivilegedPermission = mContext.checkCallingPermission(
+ CALL_PRIVILEGED) == PackageManager.PERMISSION_GRANTED;
+
+ final Intent intent = new Intent(hasCallPrivilegedPermission ?
+ Intent.ACTION_CALL_PRIVILEGED : Intent.ACTION_CALL, address);
+ Bundle updatedExtras = OutgoingCallTransaction.generateExtras(callId, extras,
+ callAttributes, mFeatureFlags);
+ // Note that this may start a potential transaction to make room for the outgoing call
+ // so we want to ensure that transaction is queued up first and then create another
+ // transaction to complete the call future.
+ CompletableFuture<Call> callFuture = mCallsManager.startOutgoingCall(address,
+ requestedAccountHandle, updatedExtras, requestedAccountHandle.getUserHandle(),
+ intent, callingPackage);
+ // The second transaction is represented below which will contain the result of whether
+ // the new outgoing call was placed or not. To simplify the logic, we will wait on the
+ // result of the outgoing call future before adding the transaction so that we can wait
+ // for the make room future to complete first.
+ if (callFuture == null) {
+ Log.d(this, "createTransactionalOutgoingCall: Outgoing call not permitted at the "
+ + "current time.");
+ return CompletableFuture.completedFuture(null);
+ }
+ return callFuture.thenComposeAsync((call) -> CompletableFuture.completedFuture(
+ new OutgoingCallTransactionSequencing(mCallsManager, callFuture,
+ mFeatureFlags)),
+ new LoggedHandlerExecutor(mHandler, "CSC.aC", mCallsManager.getLock()));
+ } else {
+ Log.d(this, "createTransactionalOutgoingCall: outgoing call not permitted at the "
+ + "current time.");
+ return CompletableFuture.completedFuture(null);
+ }
+ }
+
+ /**
+ * Processes the answer call request from the app and verifies the call state changes with
+ * sequencing provided that the calls that are being manipulated are across phone accounts.
+ * @param incomingCall The incoming call to be answered.
+ * @param videoState The video state configuration for the provided call.
+ */
public void answerCall(Call incomingCall, int videoState) {
- // Todo: call sequencing logic (stubbed)
+ Log.i(this, "answerCall: Beginning call sequencing transaction for answering "
+ + "incoming call.");
+ // Retrieve the CompletableFuture which processes the steps to make room to answer the
+ // incoming call.
+ CompletableFuture<Boolean> holdActiveForNewCallFutureHandler =
+ holdActiveCallForNewCallWithSequencing(incomingCall);
+ // If we're performing call sequencing across phone accounts, then ensure that we only
+ // proceed if the future above has completed successfully.
+ if (isProcessingCallSequencing()) {
+ holdActiveForNewCallFutureHandler.thenComposeAsync((result) -> {
+ if (result) {
+ mCallsManager.requestFocusActionAnswerCall(incomingCall, videoState);
+ } else {
+ Log.i(this, "answerCall: Hold active call transaction failed. Aborting "
+ + "request to answer the incoming call.");
+ }
+ return CompletableFuture.completedFuture(result);
+ }, new LoggedHandlerExecutor(mHandler, "CSC.aC",
+ mCallsManager.getLock()));
+ } else {
+ mCallsManager.requestFocusActionAnswerCall(incomingCall, videoState);
+ }
+ resetProcessingCallSequencing();
}
-// private CompletableFuture<Boolean> holdActiveCallForNewCallWithSequencing(Call call) {
-// // Todo: call sequencing logic (stubbed)
-// return null;
-// }
+ /**
+ * Handles the case of setting a self-managed call active with call sequencing support.
+ * @param call The self-managed call that's waiting to go active.
+ */
+ public void handleSetSelfManagedCallActive(Call call) {
+ CompletableFuture<Boolean> holdActiveCallFuture =
+ holdActiveCallForNewCallWithSequencing(call);
+ if (isProcessingCallSequencing()) {
+ holdActiveCallFuture.thenComposeAsync((result) -> {
+ if (result) {
+ Log.i(this, "markCallAsActive: requesting focus for self managed call "
+ + "before setting active.");
+ mCallsManager.requestActionSetActiveCall(call,
+ "active set explicitly for self-managed");
+ } else {
+ Log.i(this, "markCallAsActive: Unable to hold active call. "
+ + "Aborting transaction to set self managed call active.");
+ }
+ return CompletableFuture.completedFuture(result);
+ }, new LoggedHandlerExecutor(mHandler,
+ "CM.mCAA", mCallsManager.getLock()));
+ } else {
+ mCallsManager.requestActionSetActiveCall(call,
+ "active set explicitly for self-managed");
+ }
+ resetProcessingCallSequencing();
+ }
+ /**
+ * This applies to transactional calls which request to hold the active call with call
+ * sequencing support. The resulting future is an indication of whether the hold request
+ * succeeded which is then used to create additional transactions to request call focus for the
+ * new call.
+ * @param newCall The new transactional call that's waiting to go active.
+ * @param callback The callback used to report the result of holding the active call and if
+ * the new call can go active.
+ * @return The {@code CompletableFuture} indicating the result of holding the active call
+ * (if applicable).
+ */
+ public void transactionHoldPotentialActiveCallForNewCallSequencing(
+ Call newCall, OutcomeReceiver<Boolean, CallException> callback) {
+ CompletableFuture<Boolean> holdActiveCallFuture =
+ holdActiveCallForNewCallWithSequencing(newCall).thenComposeAsync((result) -> {
+ if (result) {
+ // Either we were able to hold the active call or the active call was
+ // disconnected in favor of the new call.
+ callback.onResult(true);
+ } else {
+ Log.i(this, "transactionHoldPotentialActiveCallForNewCallSequencing: "
+ + "active call could not be held or disconnected");
+ callback.onError(
+ new CallException("activeCall could not be held or disconnected",
+ CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+ }
+ return CompletableFuture.completedFuture(result);
+ }, new LoggedHandlerExecutor(mHandler, "CM.mCAA", mCallsManager.getLock()));
+ resetProcessingCallSequencing();
+ }
+
+ /**
+ * Attempts to hold the active call so that the provided call can go active. This is done via
+ * call sequencing and the resulting future is an indication of whether that request
+ * has succeeded.
+ * @param call The call that's waiting to go active.
+ * @return The {@code CompletableFuture} indicating the result of whether the active call was
+ * able to be held (if applicable).
+ */
+ CompletableFuture<Boolean> holdActiveCallForNewCallWithSequencing(Call call) {
+ Call activeCall = (Call) mCallsManager.getConnectionServiceFocusManager()
+ .getCurrentFocusCall();
+ Log.i(this, "holdActiveCallForNewCallWithSequencing, newCall: %s, "
+ + "activeCall: %s", call.getId(),
+ (activeCall == null ? "<none>" : activeCall.getId()));
+ if (activeCall != null && activeCall != call) {
+ processCallSequencing(call, activeCall);
+ if (mCallsManager.canHold(activeCall)) {
+ return activeCall.hold("swap to " + call.getId());
+ } else if (mCallsManager.supportsHold(activeCall)) {
+ // Handle the case where active call supports hold but can't currently be held.
+ // In this case, we'll look for the currently held call to disconnect prior to
+ // holding the active call.
+ // E.g.
+ // Call A - Held (Supports hold, can't hold)
+ // Call B - Active (Supports hold, can't hold)
+ // Call C - Incoming
+ // Here we need to disconnect A prior to holding B so that C can be answered.
+ // This case is driven by telephony requirements ultimately.
+ //
+ // These cases can further be broken down at the phone account level:
+ // E.g. All cases not outlined below...
+ // (1) (2)
+ // Call A (Held) - PA1 Call A (Held) - PA1
+ // Call B (Active) - PA2 Call B (Active) - PA2
+ // Call C (Incoming) - PA1 Call C (Incoming) - PA2
+ // We should ensure that only operations across phone accounts require sequencing.
+ // Otherwise, we can send the requests up til the focus call state in question.
+ Call heldCall = mCallsManager.getFirstCallWithState(CallState.ON_HOLD);
+ CompletableFuture<Boolean> disconnectFutureHandler = null;
+ // Assume default case (no sequencing required).
+ boolean areIncomingHeldFromSameSource;
+
+ if (heldCall != null) {
+ processCallSequencing(heldCall, activeCall);
+ processCallSequencing(call, heldCall);
+ areIncomingHeldFromSameSource = CallsManager.areFromSameSource(call, heldCall);
+
+ // If the calls are from the same source or the incoming call isn't a VOIP call
+ // and the held call is a carrier call, then disconnect the held call. The
+ // idea is that if we have a held carrier call and the incoming call is a
+ // VOIP call, we don't want to force the carrier call to auto-disconnect).
+ if (areIncomingHeldFromSameSource || !(call.isSelfManaged()
+ && !heldCall.isSelfManaged())) {
+ disconnectFutureHandler = heldCall.disconnect();
+ Log.i(this, "holdActiveCallForNewCallWithSequencing: "
+ + "Disconnect held call %s before holding active call %s.",
+ heldCall.getId(), activeCall.getId());
+ } else {
+ // Otherwise, fail the transaction.
+ return CompletableFuture.completedFuture(false);
+ }
+ }
+ Log.i(this, "holdActiveCallForNewCallWithSequencing: Holding active "
+ + "%s before making %s active.", activeCall.getId(), call.getId());
+
+ CompletableFuture<Boolean> holdFutureHandler;
+ if (isProcessingCallSequencing() && disconnectFutureHandler != null) {
+ holdFutureHandler = disconnectFutureHandler
+ .thenComposeAsync((result) -> {
+ if (result) {
+ return activeCall.hold();
+ }
+ return CompletableFuture.completedFuture(false);
+ }, new LoggedHandlerExecutor(mHandler,
+ "CSC.hACFNCWS", mCallsManager.getLock()));
+ } else {
+ holdFutureHandler = activeCall.hold();
+ }
+ call.increaseHeldByThisCallCount();
+ return holdFutureHandler;
+ } else {
+ // This call does not support hold. If it is from a different connection
+ // service or connection manager, then disconnect it, otherwise allow the connection
+ // service or connection manager to figure out the right states.
+ if (isProcessingCallSequencing()) {
+ Log.i(this, "holdActiveCallForNewCallWithSequencing: disconnecting %s "
+ + "so that %s can be made active.", activeCall.getId(), call.getId());
+ if (!activeCall.isEmergencyCall()) {
+ // We don't want to allow VOIP apps to disconnect carrier calls. We are
+ // purposely completing the future with false so that the call isn't
+ // answered.
+ if (call.isSelfManaged() && !activeCall.isSelfManaged()) {
+ Log.w(this, "holdActiveCallForNewCallWithSequencing: ignore "
+ + "disconnecting carrier call for making VOIP call active");
+ return CompletableFuture.completedFuture(false);
+ } else {
+ return activeCall.disconnect();
+ }
+ } else {
+ // It's not possible to hold the active call, and it's an emergency call so
+ // we will silently reject the incoming call instead of answering it.
+ Log.w(this, "holdActiveCallForNewCallWithSequencing: rejecting incoming "
+ + "call %s as the active call is an emergency call and "
+ + "it cannot be held.", call.getId());
+ return call.reject(false /* rejectWithMessage */, "" /* message */,
+ "active emergency call can't be held");
+ }
+ } else {
+ // Same source case: if the active call cannot be held, then the user has
+ // willingly chosen to accept the incoming call knowing that the active call
+ // will be disconnected.
+ return activeCall.disconnect("Active call disconnected in favor of accepting "
+ + "incoming call.");
+ }
+ }
+ }
+ return CompletableFuture.completedFuture(true);
+ }
+
+ /**
+ * Processes the unhold call request sent by the app with call sequencing support.
+ * @param call The call to be unheld.
+ */
public void unholdCall(Call call) {
- // Todo: call sequencing logic (stubbed)
+ // Cases: set active call on hold and then set this call to active
+ // Calls could be made on different phone accounts, in which case, we need to verify state
+ // change for each call.
+ CompletableFuture<Boolean> unholdCallFutureHandler = null;
+ Call activeCall = (Call) mCallsManager.getConnectionServiceFocusManager()
+ .getCurrentFocusCall();
+ if (activeCall != null && !activeCall.isLocallyDisconnecting()) {
+ // Determine whether the calls are placed on different phone accounts.
+ boolean areFromSameSource = CallsManager.areFromSameSource(activeCall, call);
+ processCallSequencing(activeCall, call);
+ boolean canHoldActiveCall = mCallsManager.canHold(activeCall);
+
+ // If the active + held call are from different phone accounts, ensure that the call
+ // sequencing states are verified at each step.
+ if (canHoldActiveCall) {
+ unholdCallFutureHandler = activeCall.hold("Swap to " + call.getId());
+ Log.addEvent(activeCall, LogUtils.Events.SWAP, "To " + call.getId());
+ Log.addEvent(call, LogUtils.Events.SWAP, "From " + activeCall.getId());
+ } else {
+ if (!areFromSameSource) {
+ // Don't unhold the call as requested if the active and held call are on
+ // different phone accounts - consider the WhatsApp (held) and PSTN (active)
+ // case. We also don't want to drop an emergency call.
+ if (!activeCall.isEmergencyCall()) {
+ Log.w(this, "unholdCall: % and %s are using different phone accounts. "
+ + "Aborting swap to %s", activeCall.getId(), call.getId(),
+ call.getId());
+ } else {
+ Log.w(this, "unholdCall: % is an emergency call, aborting swap to %s",
+ activeCall.getId(), call.getId());
+ }
+ return;
+ } else {
+ activeCall.hold("Swap to " + call.getId());
+ }
+ }
+ }
+
+ // Verify call state was changed to ACTIVE state
+ if (isProcessingCallSequencing() && unholdCallFutureHandler != null) {
+ // Only attempt to unhold call if previous request to hold/disconnect call (on different
+ // phone account) succeeded.
+ unholdCallFutureHandler.thenComposeAsync((result) -> {
+ if (result) {
+ Log.i(this, "unholdCall: Request to hold active call transaction succeeded.");
+ mCallsManager.requestActionUnholdCall(call, activeCall.getId());
+ } else {
+ Log.i(this, "unholdCall: Request to hold active call transaction failed. "
+ + "Aborting unhold transaction.");
+ }
+ return CompletableFuture.completedFuture(result);
+ }, new LoggedHandlerExecutor(mHandler, "CSC.uC",
+ mCallsManager.getLock()));
+ } else {
+ // Otherwise, we should verify call unhold succeeded for focus call.
+ mCallsManager.requestActionUnholdCall(call, activeCall.getId());
+ }
+ resetProcessingCallSequencing();
}
public CompletableFuture<Boolean> makeRoomForOutgoingCall(boolean isEmergency, Call call) {
- // Todo: call sequencing logic (stubbed)
- return CompletableFuture.completedFuture(true);
-// return isEmergency ? makeRoomForOutgoingEmergencyCall(call) : makeRoomForOutgoingCall(call);
+ CompletableFuture<Boolean> makeRoomForOutgoingCallFuture = isEmergency
+ ? makeRoomForOutgoingEmergencyCall(call)
+ : makeRoomForOutgoingCall(call);
+ resetProcessingCallSequencing();
+ return makeRoomForOutgoingCallFuture;
}
-// private CompletableFuture<Boolean> makeRoomForOutgoingEmergencyCall(Call emergencyCall) {
-// // Todo: call sequencing logic (stubbed)
-// return CompletableFuture.completedFuture(true);
-// }
+ /**
+ * This function tries to make room for the new emergency outgoing call via call sequencing.
+ * The resulting future is an indication of whether room was able to be made for the emergency
+ * call if needed.
+ * @param emergencyCall The outgoing emergency call to be placed.
+ * @return The {@code CompletableFuture} indicating the result of whether room was able to be
+ * made for the emergency call.
+ */
+ private CompletableFuture<Boolean> makeRoomForOutgoingEmergencyCall(Call emergencyCall) {
+ // Always disconnect any ringing/incoming calls when an emergency call is placed to minimize
+ // distraction. This does not affect live call count.
+ CompletableFuture<Boolean> ringingCallFuture = null;
+ Call ringingCall = null;
+ if (mCallsManager.hasRingingOrSimulatedRingingCall()) {
+ ringingCall = mCallsManager.getRingingOrSimulatedRingingCall();
+ processCallSequencing(ringingCall, emergencyCall);
+ ringingCall.getAnalytics().setCallIsAdditional(true);
+ ringingCall.getAnalytics().setCallIsInterrupted(true);
+ if (ringingCall.getState() == CallState.SIMULATED_RINGING) {
+ if (!ringingCall.hasGoneActiveBefore()) {
+ // If this is an incoming call that is currently in SIMULATED_RINGING only
+ // after a call screen, disconnect to make room and mark as missed, since
+ // the user didn't get a chance to accept/reject.
+ ringingCallFuture = ringingCall.disconnect("emergency call dialed during "
+ + "simulated ringing after screen.");
+ } else {
+ // If this is a simulated ringing call after being active and put in
+ // AUDIO_PROCESSING state again, disconnect normally.
+ ringingCallFuture = ringingCall.reject(false, null, "emergency call dialed "
+ + "during simulated ringing.");
+ }
+ } else { // normal incoming ringing call.
+ // Hang up the ringing call to make room for the emergency call and mark as missed,
+ // since the user did not reject.
+ ringingCall.setOverrideDisconnectCauseCode(
+ new DisconnectCause(DisconnectCause.MISSED));
+ ringingCallFuture = ringingCall.reject(false, null, "emergency call dialed "
+ + "during ringing.");
+ }
+ }
-// private CompletableFuture<Boolean> makeRoomForOutgoingCall(Call call) {
-// // Todo: call sequencing logic (stubbed)
-// return CompletableFuture.completedFuture(true);
-// }
+ // There is already room!
+ if (!mCallsManager.hasMaximumLiveCalls(emergencyCall)) {
+ return CompletableFuture.completedFuture(true);
+ }
-// private void resetProcessingCallSequencing() {
-// mTransactionManager.setProcessingCallSequencing(false);
-// }
+ Call liveCall = mCallsManager.getFirstCallWithLiveState();
+ Log.i(this, "makeRoomForOutgoingEmergencyCall: call = " + emergencyCall
+ + " livecall = " + liveCall);
- public CompletableFuture<Boolean> disconnectCall() {
- return CompletableFuture.completedFuture(true);
+ if (emergencyCall == liveCall) {
+ // Not likely, but a good correctness check.
+ return CompletableFuture.completedFuture(true);
+ }
+
+ if (mCallsManager.hasMaximumOutgoingCalls(emergencyCall)) {
+ Call outgoingCall = mCallsManager.getFirstCallWithState(OUTGOING_CALL_STATES);
+ String disconnectReason = null;
+ if (!outgoingCall.isEmergencyCall()) {
+ emergencyCall.getAnalytics().setCallIsAdditional(true);
+ outgoingCall.getAnalytics().setCallIsInterrupted(true);
+ disconnectReason = "Disconnecting dialing call in favor of new dialing"
+ + " emergency call.";
+ }
+ if (outgoingCall.getState() == CallState.SELECT_PHONE_ACCOUNT) {
+ // Correctness check: if there is an orphaned emergency call in the
+ // {@link CallState#SELECT_PHONE_ACCOUNT} state, just disconnect it since the user
+ // has explicitly started a new call.
+ emergencyCall.getAnalytics().setCallIsAdditional(true);
+ outgoingCall.getAnalytics().setCallIsInterrupted(true);
+ disconnectReason = "Disconnecting call in SELECT_PHONE_ACCOUNT in favor"
+ + " of new outgoing call.";
+ }
+ if (disconnectReason != null) {
+ processCallSequencing(outgoingCall, emergencyCall);
+ if (ringingCallFuture != null && isProcessingCallSequencing()) {
+ String finalDisconnectReason = disconnectReason;
+ return ringingCallFuture.thenComposeAsync((result) -> {
+ if (result) {
+ Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect"
+ + " ringing call succeeded. Attempting to disconnect "
+ + "outgoing call.");
+ return outgoingCall.disconnect(finalDisconnectReason);
+ } else {
+ Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect"
+ + "ringing call failed. Aborting attempt to disconnect "
+ + "outgoing call");
+ return CompletableFuture.completedFuture(false);
+ }
+ }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
+ mCallsManager.getLock()));
+ } else {
+ return outgoingCall.disconnect(disconnectReason);
+ }
+ }
+ // If the user tries to make two outgoing calls to different emergency call numbers,
+ // we will try to connect the first outgoing call and reject the second.
+ emergencyCall.setStartFailCause(CallFailureCause.IN_EMERGENCY_CALL);
+ return CompletableFuture.completedFuture(false);
+ }
+
+ processCallSequencing(liveCall, emergencyCall);
+ if (ringingCall != null) {
+ processCallSequencing(ringingCall, liveCall);
+ }
+ if (liveCall.getState() == CallState.AUDIO_PROCESSING) {
+ emergencyCall.getAnalytics().setCallIsAdditional(true);
+ liveCall.getAnalytics().setCallIsInterrupted(true);
+ final String disconnectReason = "disconnecting audio processing call for emergency";
+ if (ringingCallFuture != null && isProcessingCallSequencing()) {
+ return ringingCallFuture.thenComposeAsync((result) -> {
+ if (result) {
+ Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
+ + "ringing call succeeded. Attempting to disconnect live call.");
+ return liveCall.disconnect(disconnectReason);
+ } else {
+ Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
+ + "ringing call failed. Aborting attempt to disconnect live call.");
+ return CompletableFuture.completedFuture(false);
+ }
+ }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
+ mCallsManager.getLock()));
+ } else {
+ return liveCall.disconnect(disconnectReason);
+ }
+ }
+
+ // If the live call is stuck in a connecting state, prompt the user to generate a bugreport.
+ if (liveCall.getState() == CallState.CONNECTING) {
+ AnomalyReporter.reportAnomaly(LIVE_CALL_STUCK_CONNECTING_EMERGENCY_ERROR_UUID,
+ LIVE_CALL_STUCK_CONNECTING_EMERGENCY_ERROR_MSG);
+ }
+
+ // If we have the max number of held managed calls and we're placing an emergency call,
+ // we'll disconnect the ongoing call if it cannot be held.
+ if (mCallsManager.hasMaximumManagedHoldingCalls(emergencyCall)
+ && !mCallsManager.canHold(liveCall)) {
+ emergencyCall.getAnalytics().setCallIsAdditional(true);
+ liveCall.getAnalytics().setCallIsInterrupted(true);
+ // Disconnect the active call instead of the holding call because it is historically
+ // easier to do, rather than disconnect a held call.
+ final String disconnectReason = "disconnecting to make room for emergency call "
+ + emergencyCall.getId();
+ if (ringingCallFuture != null && isProcessingCallSequencing()) {
+ return ringingCallFuture.thenComposeAsync((result) -> {
+ if (result) {
+ Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
+ + "ringing call succeeded. Attempting to disconnect live call.");
+ return liveCall.disconnect(disconnectReason);
+ } else {
+ Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
+ + "ringing call failed. Aborting attempt to disconnect live call.");
+ return CompletableFuture.completedFuture(false);
+ }
+ }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
+ mCallsManager.getLock()));
+ } else {
+ return liveCall.disconnect(disconnectReason);
+ }
+ }
+
+ // TODO: Remove once b/23035408 has been corrected.
+ // If the live call is a conference, it will not have a target phone account set. This
+ // means the check to see if the live call has the same target phone account as the new
+ // call will not cause us to bail early. As a result, we'll end up holding the
+ // ongoing conference call. However, the ConnectionService is already doing that. This
+ // has caused problems with some carriers. As a workaround until b/23035408 is
+ // corrected, we will try and get the target phone account for one of the conference's
+ // children and use that instead.
+ PhoneAccountHandle liveCallPhoneAccount = liveCall.getTargetPhoneAccount();
+ if (liveCallPhoneAccount == null && liveCall.isConference() &&
+ !liveCall.getChildCalls().isEmpty()) {
+ liveCallPhoneAccount = mCallsManager.getFirstChildPhoneAccount(liveCall);
+ Log.i(this, "makeRoomForOutgoingEmergencyCall: using child call PhoneAccount = " +
+ liveCallPhoneAccount);
+ }
+
+ // We may not know which PhoneAccount the emergency call will be placed on yet, but if
+ // the liveCall PhoneAccount does not support placing emergency calls, then we know it
+ // will not be that one and we do not want multiple PhoneAccounts active during an
+ // emergency call if possible. Disconnect the active call in favor of the emergency call
+ // instead of trying to hold.
+ if (liveCall.getTargetPhoneAccount() != null) {
+ PhoneAccount pa = mCallsManager.getPhoneAccountRegistrar().getPhoneAccountUnchecked(
+ liveCall.getTargetPhoneAccount());
+ if((pa.getCapabilities() & PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS) == 0) {
+ liveCall.setOverrideDisconnectCauseCode(new DisconnectCause(
+ DisconnectCause.LOCAL, DisconnectCause.REASON_EMERGENCY_CALL_PLACED));
+ final String disconnectReason = "outgoing call does not support emergency calls, "
+ + "disconnecting.";
+ if (ringingCallFuture != null && isProcessingCallSequencing()) {
+ return ringingCallFuture.thenComposeAsync((result) -> {
+ if (result) {
+ Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
+ + "ringing call succeeded. "
+ + "Attempting to disconnect live call.");
+ return liveCall.disconnect(disconnectReason);
+ } else {
+ Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
+ + "ringing call failed. "
+ + "Aborting attempt to disconnect live call.");
+ return CompletableFuture.completedFuture(false);
+ }
+ }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
+ mCallsManager.getLock()));
+ } else {
+ return liveCall.disconnect(disconnectReason);
+ }
+ } else {
+ return CompletableFuture.completedFuture(true);
+ }
+ }
+
+ // First thing, if we are trying to make an emergency call with the same package name as
+ // the live call, then allow it so that the connection service can make its own decision
+ // about how to handle the new call relative to the current one.
+ // By default, for telephony, it will try to hold the existing call before placing the new
+ // emergency call except for if the carrier does not support holding calls for emergency.
+ // In this case, telephony will disconnect the call.
+ if (PhoneAccountHandle.areFromSamePackage(liveCallPhoneAccount,
+ emergencyCall.getTargetPhoneAccount())) {
+ Log.i(this, "makeRoomForOutgoingEmergencyCall: phoneAccount matches.");
+ emergencyCall.getAnalytics().setCallIsAdditional(true);
+ liveCall.getAnalytics().setCallIsInterrupted(true);
+ return CompletableFuture.completedFuture(true);
+ } else if (emergencyCall.getTargetPhoneAccount() == null) {
+ // Without a phone account, we can't say reliably that the call will fail.
+ // If the user chooses the same phone account as the live call, then it's
+ // still possible that the call can be made (like with CDMA calls not supporting
+ // hold but they still support adding a call by going immediately into conference
+ // mode). Return true here and we'll run this code again after user chooses an
+ // account.
+ return CompletableFuture.completedFuture(true);
+ }
+
+ // Hold the live call if possible before attempting the new outgoing emergency call.
+ if (mCallsManager.canHold(liveCall)) {
+ Log.i(this, "makeRoomForOutgoingEmergencyCall: holding live call.");
+ emergencyCall.getAnalytics().setCallIsAdditional(true);
+ emergencyCall.increaseHeldByThisCallCount();
+ liveCall.getAnalytics().setCallIsInterrupted(true);
+ final String holdReason = "calling " + emergencyCall.getId();
+ if (ringingCallFuture != null && isProcessingCallSequencing()) {
+ return ringingCallFuture.thenComposeAsync((result) -> {
+ if (result) {
+ Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
+ + "ringing call succeeded. Attempting to hold live call.");
+ return liveCall.hold(holdReason);
+ } else {
+ Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
+ + "ringing call failed. Aborting attempt to hold live call.");
+ return CompletableFuture.completedFuture(false);
+ }
+ }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
+ mCallsManager.getLock()));
+ } else {
+ return liveCall.hold(holdReason);
+ }
+ }
+
+ // The live call cannot be held so we're out of luck here. There's no room.
+ emergencyCall.setStartFailCause(CallFailureCause.CANNOT_HOLD_CALL);
+ return CompletableFuture.completedFuture(false);
+ }
+
+ /**
+ * This function tries to make room for the new outgoing call via call sequencing. The
+ * resulting future is an indication of whether room was able to be made for the call if
+ * needed.
+ * @param call The outgoing call to make room for.
+ * @return The {@code CompletableFuture} indicating the result of whether room was able to be
+ * made for the outgoing call.
+ */
+ private CompletableFuture<Boolean> makeRoomForOutgoingCall(Call call) {
+ // Already room!
+ if (!mCallsManager.hasMaximumLiveCalls(call)) {
+ return CompletableFuture.completedFuture(true);
+ }
+
+ // NOTE: If the amount of live calls changes beyond 1, this logic will probably
+ // have to change.
+ Call liveCall = mCallsManager.getFirstCallWithLiveState();
+ Log.i(this, "makeRoomForOutgoingCall call = " + call + " livecall = " +
+ liveCall);
+
+ if (call == liveCall) {
+ // If the call is already the foreground call, then we are golden.
+ // This can happen after the user selects an account in the SELECT_PHONE_ACCOUNT
+ // state since the call was already populated into the list.
+ return CompletableFuture.completedFuture(true);
+ }
+
+ CompletableFuture<Boolean> disconnectFuture = mCallsManager
+ .maybeDisconnectExistingCallForNewOutgoingCall(call, liveCall);
+ if (disconnectFuture != null) {
+ return disconnectFuture;
+ }
+
+ // TODO: Remove once b/23035408 has been corrected.
+ // If the live call is a conference, it will not have a target phone account set. This
+ // means the check to see if the live call has the same target phone account as the new
+ // call will not cause us to bail early. As a result, we'll end up holding the
+ // ongoing conference call. However, the ConnectionService is already doing that. This
+ // has caused problems with some carriers. As a workaround until b/23035408 is
+ // corrected, we will try and get the target phone account for one of the conference's
+ // children and use that instead.
+ PhoneAccountHandle liveCallPhoneAccount = liveCall.getTargetPhoneAccount();
+ if (liveCallPhoneAccount == null && liveCall.isConference() &&
+ !liveCall.getChildCalls().isEmpty()) {
+ liveCallPhoneAccount = mCallsManager.getFirstChildPhoneAccount(liveCall);
+ Log.i(this, "makeRoomForOutgoingCall: using child call PhoneAccount = " +
+ liveCallPhoneAccount);
+ }
+
+ // First thing, for managed calls, if we are trying to make a call with the same phone
+ // account as the live call, then allow it so that the connection service can make its own
+ // decision about how to handle the new call relative to the current one.
+ // Note: This behavior is primarily in place because Telephony historically manages the
+ // state of the calls it tracks by itself, holding and unholding as needed. Self-managed
+ // calls, even though from the same package are normally held/unheld automatically by
+ // Telecom. Calls within a single ConnectionService get held/unheld automatically during
+ // "swap" operations by CallsManager#holdActiveCallForNewCall. There is, however, a quirk
+ // in that if an app declares TWO different ConnectionServices, holdActiveCallForNewCall
+ // would not work correctly because focus switches between ConnectionServices, yet we
+ // tended to assume that if the calls are from the same package that the hold/unhold should
+ // be done by the app. That was a bad assumption as it meant that we could have two active
+ // calls.
+ // TODO(b/280826075): We need to come back and revisit all this logic in a holistic manner.
+ if (PhoneAccountHandle.areFromSamePackage(liveCallPhoneAccount,
+ call.getTargetPhoneAccount())
+ && !call.isSelfManaged()
+ && !liveCall.isSelfManaged()) {
+ Log.i(this, "makeRoomForOutgoingCall: managed phoneAccount matches");
+ call.getAnalytics().setCallIsAdditional(true);
+ liveCall.getAnalytics().setCallIsInterrupted(true);
+ return CompletableFuture.completedFuture(true);
+ } else if (call.getTargetPhoneAccount() == null) {
+ // Without a phone account, we can't say reliably that the call will fail.
+ // If the user chooses the same phone account as the live call, then it's
+ // still possible that the call can be made (like with CDMA calls not supporting
+ // hold but they still support adding a call by going immediately into conference
+ // mode). Return true here and we'll run this code again after user chooses an
+ // account.
+ return CompletableFuture.completedFuture(true);
+ }
+
+ // Try to hold the live call before attempting the new outgoing call.
+ if (mCallsManager.canHold(liveCall)) {
+ Log.i(this, "makeRoomForOutgoingCall: holding live call.");
+ call.getAnalytics().setCallIsAdditional(true);
+ liveCall.getAnalytics().setCallIsInterrupted(true);
+ return liveCall.hold("calling " + call.getId());
+ }
+
+ // The live call cannot be held so we're out of luck here. There's no room.
+ call.setStartFailCause(CallFailureCause.CANNOT_HOLD_CALL);
+ return CompletableFuture.completedFuture(false);
+ }
+
+ /**
+ * Processes the request from the app to disconnect a call. This is done via call sequencing
+ * so that Telecom properly cleans up the call locally provided that the call has been
+ * properly disconnected on the connection side.
+ * @param call The call to disconnect.
+ * @param previousState The previous state of the call before disconnecting.
+ */
+ public void disconnectCall(Call call, int previousState) {
+ CompletableFuture<Boolean> disconnectFuture = call.disconnect();
+ disconnectFuture.thenComposeAsync((result) -> {
+ if (result) {
+ Log.i(this, "disconnectCall: Disconnect call transaction succeeded. "
+ + "Processing associated cleanup.");
+ mCallsManager.processDisconnectCallAndCleanup(call, previousState);
+ } else {
+ Log.i(this, "disconnectCall: Disconnect call transaction failed. "
+ + "Aborting associated cleanup.");
+ }
+ return CompletableFuture.completedFuture(false);
+ }, new LoggedHandlerExecutor(mHandler, "CSC.dC",
+ mCallsManager.getLock()));
+ }
+
+ private void resetProcessingCallSequencing() {
+ setProcessingCallSequencing(false);
+ }
+
+ private void setProcessingCallSequencing(boolean processingCallSequencing) {
+ mProcessingCallSequencing = processingCallSequencing;
+ }
+
+ /**
+ * Checks if the 2 calls provided are from the same source and sets the
+ * mProcessingCallSequencing field if they aren't in order to signal that sequencing is
+ * required to verify the call state changes.
+ */
+ private void processCallSequencing(Call call1, Call call2) {
+ boolean areCallsFromSameSource = CallsManager.areFromSameSource(call1, call2);
+ if (!areCallsFromSameSource) {
+ setProcessingCallSequencing(true);
+ }
+ }
+
+ public boolean isProcessingCallSequencing() {
+ return mProcessingCallSequencing;
+ }
+
+ public Handler getHandler() {
+ return mHandler;
}
}
diff --git a/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java b/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java
index 8410c54..df0837d 100644
--- a/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java
+++ b/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java
@@ -16,8 +16,18 @@
package com.android.server.telecom.callsequencing;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.OutcomeReceiver;
+import android.telecom.CallAttributes;
+import android.telecom.CallException;
+import android.telecom.Log;
+
import com.android.server.telecom.Call;
import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.LoggedHandlerExecutor;
+import com.android.server.telecom.callsequencing.voip.OutgoingCallTransaction;
+import com.android.server.telecom.flags.FeatureFlags;
import java.util.concurrent.CompletableFuture;
@@ -29,16 +39,47 @@
private final CallsManager mCallsManager;
private final CallSequencingController mSequencingController;
+ private final Handler mHandler;
+ private final FeatureFlags mFeatureFlags;
private final boolean mIsCallSequencingEnabled;
public CallsManagerCallSequencingAdapter(CallsManager callsManager,
CallSequencingController sequencingController,
- boolean isCallSequencingEnabled) {
+ FeatureFlags featureFlags) {
mCallsManager = callsManager;
mSequencingController = sequencingController;
- mIsCallSequencingEnabled = isCallSequencingEnabled;
+ mHandler = sequencingController.getHandler();
+ mFeatureFlags = featureFlags;
+ mIsCallSequencingEnabled = featureFlags.enableCallSequencing();
}
+ /**
+ * Helps create the transaction representing the outgoing transactional call. For outgoing
+ * calls, there can be more than one transaction that will need to complete when
+ * mIsCallSequencingEnabled is true. Otherwise, rely on the old behavior of creating an
+ * {@link OutgoingCallTransaction}.
+ * @param callAttributes The call attributes associated with the call.
+ * @param extras The extras that are associated with the call.
+ * @param callingPackage The calling package representing where the request was invoked from.
+ * @return The {@link CompletableFuture<CallTransaction>} that encompasses the request to
+ * place/receive the transactional call.
+ */
+ public CompletableFuture<CallTransaction> createTransactionalOutgoingCall(String callId,
+ CallAttributes callAttributes, Bundle extras, String callingPackage) {
+ return mIsCallSequencingEnabled
+ ? mSequencingController.createTransactionalOutgoingCall(callId,
+ callAttributes, extras, callingPackage)
+ : CompletableFuture.completedFuture(new OutgoingCallTransaction(callId,
+ mCallsManager.getContext(), callAttributes, mCallsManager, extras,
+ mFeatureFlags));
+ }
+
+ /**
+ * Conditionally try to answer the call depending on whether call sequencing
+ * (mIsCallSequencingEnabled) is enabled.
+ * @param incomingCall The incoming call that should be answered.
+ * @param videoState The video state configuration associated with the call.
+ */
public void answerCall(Call incomingCall, int videoState) {
if (mIsCallSequencingEnabled && !incomingCall.isTransactionalCall()) {
mSequencingController.answerCall(incomingCall, videoState);
@@ -47,6 +88,11 @@
}
}
+ /**
+ * Conditionally attempt to unhold the provided call depending on whether call sequencing
+ * (mIsCallSequencingEnabled) is enabled.
+ * @param call The call to unhold.
+ */
public void unholdCall(Call call) {
if (mIsCallSequencingEnabled) {
mSequencingController.unholdCall(call);
@@ -55,34 +101,111 @@
}
}
+ /**
+ * Conditionally attempt to hold the provided call depending on whether call sequencing
+ * (mIsCallSequencingEnabled) is enabled.
+ * @param call The call to hold.
+ */
public void holdCall(Call call) {
// Sequencing already taken care of for CSW/TSW in Call class.
- call.hold();
+ CompletableFuture<Boolean> holdFuture = call.hold();
+ if (mIsCallSequencingEnabled) {
+ logFutureResultTransaction(holdFuture, "holdCall", "CMCSA.hC",
+ "hold call transaction succeeded.", "hold call transaction failed.");
+ }
}
- public void unholdCallForRemoval(Call removedCall,
- boolean isLocallyDisconnecting) {
- // Todo: confirm verification of disconnect logic
- // Sequencing already taken care of for CSW/TSW in Call class.
- mCallsManager.maybeMoveHeldCallToForeground(removedCall, isLocallyDisconnecting);
+ /**
+ * Conditionally disconnect the provided call depending on whether call sequencing
+ * (mIsCallSequencingEnabled) is enabled. The sequencing functionality ensures that we wait for
+ * the call to be disconnected as signalled by CSW/TSW as to ensure that subsequent call
+ * operations don't overlap with this one.
+ * @param call The call to disconnect.
+ */
+ public void disconnectCall(Call call) {
+ int previousState = call.getState();
+ if (mIsCallSequencingEnabled) {
+ mSequencingController.disconnectCall(call, previousState);
+ } else {
+ mCallsManager.disconnectCallOld(call, previousState);
+ }
}
+ /**
+ * Conditionally make room for the outgoing call depending on whether call sequencing
+ * (mIsCallSequencingEnabled) is enabled.
+ * @param isEmergency Indicator of whether the call is an emergency call.
+ * @param call The call to potentially make room for.
+ * @return {@link CompletableFuture} which will contain the result of the transaction if room
+ * was able to made for the call.
+ */
public CompletableFuture<Boolean> makeRoomForOutgoingCall(boolean isEmergency, Call call) {
if (mIsCallSequencingEnabled) {
return mSequencingController.makeRoomForOutgoingCall(isEmergency, call);
} else {
return isEmergency
? CompletableFuture.completedFuture(
- makeRoomForOutgoingEmergencyCallFlagOff(call))
- : CompletableFuture.completedFuture(makeRoomForOutgoingCallFlagOff(call));
+ mCallsManager.makeRoomForOutgoingEmergencyCall(call))
+ : CompletableFuture.completedFuture(
+ mCallsManager.makeRoomForOutgoingCall(call));
}
}
- private boolean makeRoomForOutgoingCallFlagOff(Call call) {
- return mCallsManager.makeRoomForOutgoingCall(call);
+ /**
+ * Attempts to mark the self-managed call as active by first holding the active call and then
+ * requesting call focus for the self-managed call.
+ * @param call The self-managed call to set active
+ */
+ public void markCallAsActiveSelfManagedCall(Call call) {
+ if (mIsCallSequencingEnabled) {
+ mSequencingController.handleSetSelfManagedCallActive(call);
+ } else {
+ mCallsManager.holdActiveCallForNewCall(call);
+ mCallsManager.requestActionSetActiveCall(call,
+ "active set explicitly for self-managed");
+ }
}
- private boolean makeRoomForOutgoingEmergencyCallFlagOff(Call call) {
- return mCallsManager.makeRoomForOutgoingEmergencyCall(call);
+ /**
+ * Attempts to hold the active call for transactional call cases with call sequencing support
+ * if mIsCallSequencingEnabled is true.
+ * @param newCall The new (transactional) call that's waiting to go active.
+ * @param activeCall The currently active call.
+ * @param callback The callback to report the result of the aforementioned hold transaction.
+ * @return {@code CompletableFuture} indicating the result of holding the active call.
+ */
+ public void transactionHoldPotentialActiveCallForNewCall(Call newCall,
+ Call activeCall, OutcomeReceiver<Boolean, CallException> callback) {
+ if (mIsCallSequencingEnabled) {
+ mSequencingController.transactionHoldPotentialActiveCallForNewCallSequencing(
+ newCall, callback);
+ } else {
+ mCallsManager.transactionHoldPotentialActiveCallForNewCallOld(newCall,
+ activeCall, callback);
+ }
+ }
+
+ /**
+ * Generic helper to log the result of the {@link CompletableFuture} containing the transactions
+ * that are being processed in the context of call sequencing.
+ * @param future The {@link CompletableFuture} encompassing the transaction that's being
+ * computed.
+ * @param methodName The method name to describe the type of transaction being processed.
+ * @param sessionName The session name to identify the log.
+ * @param successMsg The message to be logged if the transaction succeeds.
+ * @param failureMsg The message to be logged if the transaction fails.
+ */
+ public void logFutureResultTransaction(CompletableFuture<Boolean> future, String methodName,
+ String sessionName, String successMsg, String failureMsg) {
+ future.thenApplyAsync((result) -> {
+ StringBuilder msg = new StringBuilder(methodName).append(": ");
+ msg.append(result ? successMsg : failureMsg);
+ Log.i(this, String.valueOf(msg));
+ return CompletableFuture.completedFuture(result);
+ }, new LoggedHandlerExecutor(mHandler, sessionName, mCallsManager.getLock()));
+ }
+
+ public Handler getHandler() {
+ return mHandler;
}
}
diff --git a/src/com/android/server/telecom/callsequencing/TransactionManager.java b/src/com/android/server/telecom/callsequencing/TransactionManager.java
index a3b3828..2a6431b 100644
--- a/src/com/android/server/telecom/callsequencing/TransactionManager.java
+++ b/src/com/android/server/telecom/callsequencing/TransactionManager.java
@@ -169,14 +169,6 @@
}
}
- public void setProcessingCallSequencing(boolean processingCallSequencing) {
- mProcessingCallSequencing = processingCallSequencing;
- }
-
- public boolean isProcessingCallSequencing() {
- return mProcessingCallSequencing;
- }
-
/**
* Called when the dumpsys is created for telecom to capture the current state.
*/
diff --git a/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java b/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java
index 7c8bbe4..570c2cc 100644
--- a/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java
+++ b/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java
@@ -40,14 +40,13 @@
public class TransactionalCallSequencingAdapter {
private final TransactionManager mTransactionManager;
private final CallsManager mCallsManager;
-// private final boolean mIsCallSequencingEnabled;
+ private final boolean mIsCallSequencingEnabled;
public TransactionalCallSequencingAdapter(TransactionManager transactionManager,
CallsManager callsManager, boolean isCallSequencingEnabled) {
mTransactionManager = transactionManager;
mCallsManager = callsManager;
- // TODO implement call sequencing changes
-// mIsCallSequencingEnabled = isCallSequencingEnabled;
+ mIsCallSequencingEnabled = isCallSequencingEnabled;
}
/**
@@ -55,7 +54,13 @@
*/
public void setActive(Call call,
OutcomeReceiver<CallTransactionResult, CallException> receiver) {
- setActiveFlagOff(call, receiver);
+ if (mIsCallSequencingEnabled) {
+ createSetActiveTransactionSequencing(call, true /* callControlRequest */, null,
+ receiver, receiver);
+ } else {
+ mTransactionManager.addTransaction(createSetActiveTransactions(call,
+ true /* callControlRequest */), receiver);
+ }
}
/**
@@ -63,7 +68,18 @@
*/
public void setAnswered(Call call, int newVideoState,
OutcomeReceiver<CallTransactionResult, CallException> receiver) {
- setAnsweredFlagOff(call, newVideoState, receiver);
+ boolean isCallControlRequest = true;
+ OutcomeReceiver<CallTransactionResult, CallException> receiverForTransaction =
+ getSetAnswerReceiver(call, null /* foregroundCallBeforeSwap */,
+ false /* wasForegroundActive */, newVideoState, receiver,
+ isCallControlRequest);
+ if (mIsCallSequencingEnabled) {
+ createSetActiveTransactionSequencing(call, isCallControlRequest, null,
+ receiver, receiverForTransaction /* receiverForTransaction */);
+ } else {
+ mTransactionManager.addTransaction(createSetActiveTransactions(call,
+ isCallControlRequest), receiverForTransaction);
+ }
}
/**
@@ -71,7 +87,8 @@
*/
public void setDisconnected(Call call, DisconnectCause dc,
OutcomeReceiver<CallTransactionResult, CallException> receiver) {
- setDisconnectedFlagOff(call, dc, receiver);
+ mTransactionManager.addTransaction(
+ new EndCallTransaction(mCallsManager, dc, call), receiver);
}
/**
@@ -79,7 +96,7 @@
*/
public void setInactive(Call call,
OutcomeReceiver<CallTransactionResult, CallException> receiver) {
- setInactiveFlagOff(call, receiver);
+ mTransactionManager.addTransaction(new HoldCallTransaction(mCallsManager,call), receiver);
}
/**
@@ -89,16 +106,52 @@
public CompletableFuture<Boolean> onSetActive(Call call,
CallTransaction clientCbT,
OutcomeReceiver<CallTransactionResult, CallException> receiver) {
- return onSetActiveFlagOff(call, clientCbT, receiver);
+ // save CallsManager state before sending client state changes
+ Call foregroundCallBeforeSwap = mCallsManager.getForegroundCall();
+ boolean wasActive = foregroundCallBeforeSwap != null && foregroundCallBeforeSwap.isActive();
+ OutcomeReceiver<CallTransactionResult, CallException> receiverForTransaction =
+ getOnSetActiveReceiver(call, foregroundCallBeforeSwap, wasActive, receiver);
+
+ if (mIsCallSequencingEnabled) {
+ return createSetActiveTransactionSequencing(call, false /* callControlRequest */,
+ clientCbT, receiver, receiverForTransaction);
+ } else {
+ SerialTransaction serialTransactions = createSetActiveTransactions(call,
+ false /* callControlRequest */);
+ serialTransactions.appendTransaction(clientCbT);
+ // do CallsManager workload before asking client and
+ // reset CallsManager state if client does NOT ack
+ return mTransactionManager.addTransaction(
+ serialTransactions, receiverForTransaction);
+ }
}
/**
* Server -> Client command to answer an incoming call, which if it fails, will trigger the
* disconnect of the call and then reset the state of the other call back to what it was before.
*/
- public void onSetAnswered(Call call, int videoState, CallTransaction clientCbT,
- OutcomeReceiver<CallTransactionResult, CallException> receiver) {
- onSetAnsweredFlagOff(call, videoState, clientCbT, receiver);
+ public CompletableFuture<Boolean> onSetAnswered(Call call, int videoState,
+ CallTransaction clientCbT, OutcomeReceiver<CallTransactionResult,
+ CallException> receiver) {
+ boolean isCallControlRequest = false;
+ // save CallsManager state before sending client state changes
+ Call foregroundCallBeforeSwap = mCallsManager.getForegroundCall();
+ boolean wasActive = foregroundCallBeforeSwap != null && foregroundCallBeforeSwap.isActive();
+ OutcomeReceiver<CallTransactionResult, CallException> receiverForTransaction =
+ getSetAnswerReceiver(call, foregroundCallBeforeSwap, wasActive,
+ videoState, receiver, isCallControlRequest);
+
+ if (mIsCallSequencingEnabled) {
+ return createSetActiveTransactionSequencing(call, false /* callControlRequest */,
+ clientCbT, receiver, receiverForTransaction);
+ } else {
+ SerialTransaction serialTransactions = createSetActiveTransactions(call,
+ isCallControlRequest);
+ serialTransactions.appendTransaction(clientCbT);
+ // do CallsManager workload before asking client and
+ // reset CallsManager state if client does NOT ack
+ return mTransactionManager.addTransaction(serialTransactions, receiverForTransaction);
+ }
}
/**
@@ -107,7 +160,19 @@
public CompletableFuture<Boolean> onSetInactive(Call call,
CallTransaction clientCbT,
OutcomeReceiver<CallTransactionResult, CallException> receiver) {
- return onSetInactiveFlagOff(call, clientCbT, receiver);
+ return mTransactionManager.addTransaction(clientCbT,
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(CallTransactionResult callTransactionResult) {
+ mCallsManager.markCallAsOnHold(call);
+ receiver.onResult(callTransactionResult);
+ }
+
+ @Override
+ public void onError(CallException error) {
+ receiver.onError(error);
+ }
+ });
}
/**
@@ -116,7 +181,20 @@
public CompletableFuture<Boolean> onSetDisconnected(Call call,
DisconnectCause dc, CallTransaction clientCbT, OutcomeReceiver<CallTransactionResult,
CallException> receiver) {
- return onSetDisconnectedFlagOff(call, dc, clientCbT, receiver);
+ return mTransactionManager.addTransaction(clientCbT,
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(CallTransactionResult result) {
+ removeCallFromCallsManager(call, dc);
+ receiver.onResult(result);
+ }
+
+ @Override
+ public void onError(CallException exception) {
+ removeCallFromCallsManager(call, dc);
+ receiver.onError(exception);
+ }
+ });
}
/**
@@ -126,146 +204,6 @@
cleanupFlagOff(calls);
}
- private void setActiveFlagOff(Call call,
- OutcomeReceiver<CallTransactionResult, CallException> receiver) {
- CompletableFuture<Boolean> transactionResult = mTransactionManager
- .addTransaction(createSetActiveTransactions(call,
- true /* callControlRequest */), receiver);
- }
-
- private void setAnsweredFlagOff(Call call, int newVideoState,
- OutcomeReceiver<CallTransactionResult, CallException> receiver) {
- CompletableFuture<Boolean> transactionResult = mTransactionManager
- .addTransaction(createSetActiveTransactions(call,
- true /* callControlRequest */),
- new OutcomeReceiver<>() {
- @Override
- public void onResult(CallTransactionResult callTransactionResult) {
- call.setVideoState(newVideoState);
- receiver.onResult(callTransactionResult);
- }
-
- @Override
- public void onError(CallException error) {
- receiver.onError(error);
- }
- });
- }
-
- private void setDisconnectedFlagOff(Call call, DisconnectCause dc,
- OutcomeReceiver<CallTransactionResult, CallException> receiver) {
- CompletableFuture<Boolean> transactionResult = mTransactionManager
- .addTransaction(new EndCallTransaction(mCallsManager,
- dc, call), receiver);
- }
-
- private void setInactiveFlagOff(Call call,
- OutcomeReceiver<CallTransactionResult, CallException> receiver) {
- CompletableFuture<Boolean> transactionResult = mTransactionManager
- .addTransaction(new HoldCallTransaction(mCallsManager,call), receiver);
- }
-
- private CompletableFuture<Boolean> onSetActiveFlagOff(Call call,
- CallTransaction clientCbT,
- OutcomeReceiver<CallTransactionResult, CallException> receiver) {
- // save CallsManager state before sending client state changes
- Call foregroundCallBeforeSwap = mCallsManager.getForegroundCall();
- boolean wasActive = foregroundCallBeforeSwap != null && foregroundCallBeforeSwap.isActive();
- SerialTransaction serialTransactions = createSetActiveTransactions(call,
- false /* callControlRequest */);
- serialTransactions.appendTransaction(clientCbT);
- // do CallsManager workload before asking client and
- // reset CallsManager state if client does NOT ack
- return mTransactionManager.addTransaction(
- serialTransactions,
- new OutcomeReceiver<>() {
- @Override
- public void onResult(CallTransactionResult result) {
- receiver.onResult(result);
- }
-
- @Override
- public void onError(CallException exception) {
- mCallsManager.markCallAsOnHold(call);
- maybeResetForegroundCall(foregroundCallBeforeSwap, wasActive);
- receiver.onError(exception);
- }
- });
- }
-
- private void onSetAnsweredFlagOff(Call call, int videoState, CallTransaction clientCbT,
- OutcomeReceiver<CallTransactionResult, CallException> receiver) {
- // save CallsManager state before sending client state changes
- Call foregroundCallBeforeSwap = mCallsManager.getForegroundCall();
- boolean wasActive = foregroundCallBeforeSwap != null && foregroundCallBeforeSwap.isActive();
- SerialTransaction serialTransactions = createSetActiveTransactions(call,
- false /* callControlRequest */);
- serialTransactions.appendTransaction(clientCbT);
- // do CallsManager workload before asking client and
- // reset CallsManager state if client does NOT ack
- CompletableFuture<Boolean> transactionResult = mTransactionManager
- .addTransaction(serialTransactions,
- new OutcomeReceiver<>() {
- @Override
- public void onResult(CallTransactionResult result) {
- call.setVideoState(videoState);
- receiver.onResult(result);
- }
-
- @Override
- public void onError(CallException exception) {
- // This also sends the signal to untrack from TSW and the client_TSW
- removeCallFromCallsManager(call,
- new DisconnectCause(DisconnectCause.REJECTED,
- "client rejected to answer the call;"
- + " force disconnecting"));
- maybeResetForegroundCall(foregroundCallBeforeSwap, wasActive);
- receiver.onError(exception);
- }
- });
- }
-
- private CompletableFuture<Boolean> onSetInactiveFlagOff(Call call,
- CallTransaction clientCbT,
- OutcomeReceiver<CallTransactionResult, CallException> receiver) {
- return mTransactionManager.addTransaction(clientCbT,
- new OutcomeReceiver<>() {
- @Override
- public void onResult(CallTransactionResult callTransactionResult) {
- mCallsManager.markCallAsOnHold(call);
- receiver.onResult(callTransactionResult);
- }
-
- @Override
- public void onError(CallException error) {
- receiver.onError(error);
- }
- });
- }
-
- /**
- * Server -> Client command to disconnect the call
- */
- private CompletableFuture<Boolean> onSetDisconnectedFlagOff(Call call,
- DisconnectCause dc, CallTransaction clientCbT,
- OutcomeReceiver<CallTransactionResult, CallException> receiver) {
- return mTransactionManager.addTransaction(clientCbT,
- new OutcomeReceiver<>() {
- @Override
- public void onResult(CallTransactionResult result) {
- removeCallFromCallsManager(call, dc);
- receiver.onResult(result);
- }
-
- @Override
- public void onError(CallException exception) {
- removeCallFromCallsManager(call, dc);
- receiver.onError(exception);
- }
- }
- );
- }
-
private SerialTransaction createSetActiveTransactions(Call call, boolean isCallControlRequest) {
// create list for multiple transactions
List<CallTransaction> transactions = new ArrayList<>();
@@ -279,6 +217,48 @@
return new SerialTransaction(transactions, mCallsManager.getLock());
}
+ /**
+ * This code path is invoked when mIsCallSequencingEnabled is true. We will first try to hold
+ * the active call before adding the transactions to request call focus for the new call as well
+ * as verify the client ack for the transaction (if applicable). If the hold transaction
+ * succeeds, we will continue processing the rest of the transactions via a SerialTransaction.
+ */
+ private CompletableFuture<Boolean> createSetActiveTransactionSequencing(
+ Call call, boolean isCallControlRequest, CallTransaction clientCbT,
+ OutcomeReceiver<CallTransactionResult, CallException> receiver,
+ OutcomeReceiver<CallTransactionResult, CallException> receiverForTransaction) {
+ final CompletableFuture<Boolean>[] createSetActiveFuture =
+ new CompletableFuture[]{new CompletableFuture<>()};
+ OutcomeReceiver<Boolean, CallException> maybePerformHoldCallback = new OutcomeReceiver<>() {
+ @Override
+ public void onResult(Boolean result) {
+ // Transaction not yet completed. Still need to request focus for active call and
+ // process client callback transaction if applicable.
+ // create list for multiple transactions
+ List<CallTransaction> transactions = new ArrayList<>();
+ // And request a new focus call update
+ transactions.add(new RequestNewActiveCallTransaction(mCallsManager, call));
+ if (clientCbT != null){
+ transactions.add(clientCbT);
+ }
+ SerialTransaction serialTransactions = new SerialTransaction(
+ transactions, mCallsManager.getLock());
+ createSetActiveFuture[0] = mTransactionManager.addTransaction(serialTransactions,
+ receiverForTransaction);
+ }
+
+ @Override
+ public void onError(CallException exception) {
+ createSetActiveFuture[0] = CompletableFuture.completedFuture(false);
+ receiver.onError(exception);
+ }
+ };
+
+ mCallsManager.transactionHoldPotentialActiveCallForNewCall(call,
+ isCallControlRequest, maybePerformHoldCallback);
+ return createSetActiveFuture[0];
+ }
+
private void removeCallFromCallsManager(Call call, DisconnectCause cause) {
if (cause.getCode() != DisconnectCause.REJECTED) {
mCallsManager.markCallAsDisconnected(call, cause);
@@ -301,4 +281,49 @@
mCallsManager.removeCall(call); // This will clear mTrackedCalls && ClientTWS
}
}
+
+ private OutcomeReceiver<CallTransactionResult, CallException> getOnSetActiveReceiver(
+ Call call, Call foregroundCallBeforeSwap, boolean wasForegroundActive,
+ OutcomeReceiver<CallTransactionResult, CallException> receiver) {
+ return new OutcomeReceiver<>() {
+ @Override
+ public void onResult(CallTransactionResult result) {
+ receiver.onResult(result);
+ }
+
+ @Override
+ public void onError(CallException exception) {
+ mCallsManager.markCallAsOnHold(call);
+ maybeResetForegroundCall(foregroundCallBeforeSwap, wasForegroundActive);
+ receiver.onError(exception);
+ }
+ };
+ }
+
+ private OutcomeReceiver<CallTransactionResult, CallException> getSetAnswerReceiver(
+ Call call, Call foregroundCallBeforeSwap, boolean wasForegroundActive, int videoState,
+ OutcomeReceiver<CallTransactionResult, CallException> receiver,
+ boolean isCallControlRequest) {
+ return new OutcomeReceiver<>() {
+ @Override
+ public void onResult(CallTransactionResult result) {
+ call.setVideoState(videoState);
+ receiver.onResult(result);
+ }
+
+ @Override
+ public void onError(CallException exception) {
+ if (!isCallControlRequest) {
+ // This also sends the signal to untrack from TSW and the
+ // client_TSW
+ removeCallFromCallsManager(call,
+ new DisconnectCause(DisconnectCause.REJECTED,
+ "client rejected to answer the call;"
+ + " force disconnecting"));
+ maybeResetForegroundCall(foregroundCallBeforeSwap, wasForegroundActive);
+ }
+ receiver.onError(exception);
+ }
+ };
+ }
}
diff --git a/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransaction.java b/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransaction.java
index 572de55..b221579 100644
--- a/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransaction.java
@@ -91,7 +91,7 @@
CompletableFuture<Call> callFuture =
mCallsManager.startOutgoingCall(mCallAttributes.getAddress(),
mCallAttributes.getPhoneAccountHandle(),
- generateExtras(mCallAttributes),
+ generateExtras(mCallId, mExtras, mCallAttributes, mFeatureFlags),
mCallAttributes.getPhoneAccountHandle().getUserHandle(),
intent,
mCallingPackage);
@@ -102,35 +102,11 @@
CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
"incoming call not permitted at the current time"));
}
- CompletionStage<CallTransactionResult> result = callFuture.thenComposeAsync(
- (call) -> {
- Log.d(TAG, "processTransaction: completing future");
-
- if (call == null) {
- Log.d(TAG, "processTransaction: call is null");
- return CompletableFuture.completedFuture(
- new CallTransactionResult(
- CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
- "call could not be created at this time"));
- } else {
- Log.d(TAG, "processTransaction: call done. id=" + call.getId());
- }
-
- if (mFeatureFlags.disconnectSelfManagedStuckStartupCalls()) {
- // set to dialing so the CallAnomalyWatchdog gives the VoIP calls 1
- // minute to timeout rather than 5 seconds.
- mCallsManager.markCallAsDialing(call);
- }
-
- return CompletableFuture.completedFuture(
- new CallTransactionResult(
- CallTransactionResult.RESULT_SUCCEED,
- call, null, true));
- }
+ return callFuture.thenComposeAsync(
+ (call) -> processOutgoingCallTransactionHelper(call, TAG,
+ mCallsManager, mFeatureFlags)
, new LoggedHandlerExecutor(mHandler, "OCT.pT", null));
-
- return result;
} else {
return CompletableFuture.completedFuture(
new CallTransactionResult(
@@ -141,20 +117,47 @@
}
@VisibleForTesting
- public Bundle generateExtras(CallAttributes callAttributes) {
- mExtras.setDefusable(true);
- mExtras.putString(TelecomManager.TRANSACTION_CALL_ID_KEY, mCallId);
- mExtras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
- if (mFeatureFlags.transactionalVideoState()) {
+ public static Bundle generateExtras(String callId, Bundle extras,
+ CallAttributes callAttributes, FeatureFlags featureFlags) {
+ extras.setDefusable(true);
+ extras.putString(TelecomManager.TRANSACTION_CALL_ID_KEY, callId);
+ extras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
+ if (featureFlags.transactionalVideoState()) {
// Transactional calls need to remap the CallAttributes video state to the existing
// VideoProfile for consistency.
- mExtras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+ extras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
TransactionalVideoStateToVideoProfileState(callAttributes.getCallType()));
} else {
- mExtras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+ extras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
callAttributes.getCallType());
}
- mExtras.putCharSequence(DISPLAY_NAME_KEY, callAttributes.getDisplayName());
- return mExtras;
+ extras.putCharSequence(DISPLAY_NAME_KEY, callAttributes.getDisplayName());
+ return extras;
+ }
+
+ public static CompletableFuture<CallTransactionResult> processOutgoingCallTransactionHelper(
+ Call call, String tag, CallsManager callsManager, FeatureFlags featureFlags) {
+ Log.d(tag, "processTransaction: completing future");
+
+ if (call == null) {
+ Log.d(tag, "processTransaction: call is null");
+ return CompletableFuture.completedFuture(
+ new CallTransactionResult(
+ CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
+ "call could not be created at this time"));
+ } else {
+ Log.d(tag, "processTransaction: call done. id=" + call.getId());
+ }
+
+ if (featureFlags.disconnectSelfManagedStuckStartupCalls()) {
+ // set to dialing so the CallAnomalyWatchdog gives the VoIP calls 1
+ // minute to timeout rather than 5 seconds.
+ callsManager.markCallAsDialing(call);
+ }
+
+ return CompletableFuture.completedFuture(
+ new CallTransactionResult(
+ CallTransactionResult.RESULT_SUCCEED,
+ call, null, true));
}
}
diff --git a/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransactionSequencing.java b/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransactionSequencing.java
new file mode 100644
index 0000000..c38b55d
--- /dev/null
+++ b/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransactionSequencing.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.callsequencing.voip;
+
+import static android.telecom.CallException.CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME;
+
+import android.util.Log;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.LoggedHandlerExecutor;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
+import com.android.server.telecom.flags.FeatureFlags;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+public class OutgoingCallTransactionSequencing extends CallTransaction {
+
+ private static final String TAG = OutgoingCallTransactionSequencing.class.getSimpleName();
+ private final CompletableFuture<Call> mCallFuture;
+ private final CallsManager mCallsManager;
+ private FeatureFlags mFeatureFlags;
+
+ public OutgoingCallTransactionSequencing(CallsManager callsManager,
+ CompletableFuture<Call> callFuture, FeatureFlags featureFlags) {
+ super(callsManager.getLock());
+ mCallsManager = callsManager;
+ mCallFuture = callFuture;
+ mFeatureFlags = featureFlags;
+ }
+
+ @Override
+ public CompletionStage<CallTransactionResult> processTransaction(Void v) {
+ Log.d(TAG, "processTransaction");
+ if (mCallFuture == null) {
+ return CompletableFuture.completedFuture(
+ new CallTransactionResult(
+ CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
+ "outgoing call not permitted at the current time"));
+ }
+
+ return mCallFuture.thenComposeAsync(
+ (call) -> OutgoingCallTransaction.processOutgoingCallTransactionHelper(call, TAG,
+ mCallsManager, mFeatureFlags)
+ , new LoggedHandlerExecutor(mHandler, "OCT.pT", null));
+ }
+}
diff --git a/src/com/android/server/telecom/components/TelecomService.java b/src/com/android/server/telecom/components/TelecomService.java
index 2d8c78e..4db3e14 100644
--- a/src/com/android/server/telecom/components/TelecomService.java
+++ b/src/com/android/server/telecom/components/TelecomService.java
@@ -81,10 +81,11 @@
Log.d(this, "onBind");
return new ITelecomLoader.Stub() {
@Override
- public ITelecomService createTelecomService(IInternalServiceRetriever retriever) {
+ public ITelecomService createTelecomService(IInternalServiceRetriever retriever,
+ String sysUiPackageName) {
InternalServiceRetrieverAdapter adapter =
new InternalServiceRetrieverAdapter(retriever);
- initializeTelecomSystem(TelecomService.this, adapter);
+ initializeTelecomSystem(TelecomService.this, adapter, sysUiPackageName);
synchronized (getTelecomSystem().getLock()) {
return getTelecomSystem().getTelecomServiceImpl().getBinder();
}
@@ -103,7 +104,7 @@
* @param context
*/
static void initializeTelecomSystem(Context context,
- InternalServiceRetrieverAdapter internalServiceRetriever) {
+ InternalServiceRetrieverAdapter internalServiceRetriever, String sysUiPackageName) {
if (TelecomSystem.getInstance() == null) {
FeatureFlags featureFlags = new FeatureFlagsImpl();
NotificationChannelManager notificationChannelManager =
@@ -204,6 +205,7 @@
(RoleManager) context.getSystemService(Context.ROLE_SERVICE)),
new ContactsAsyncHelper.Factory(),
internalServiceRetriever.getDeviceIdleController(),
+ sysUiPackageName,
new Ringer.AccessibilityManagerAdapter() {
@Override
public boolean startFlashNotificationSequence(
diff --git a/src/com/android/server/telecom/metrics/ApiStats.java b/src/com/android/server/telecom/metrics/ApiStats.java
index f0b5dc7..4b23e47 100644
--- a/src/com/android/server/telecom/metrics/ApiStats.java
+++ b/src/com/android/server/telecom/metrics/ApiStats.java
@@ -191,6 +191,8 @@
Arrays.stream(mPulledAtoms.telecomApiStats).forEach(v -> data.add(
TelecomStatsLog.buildStatsEvent(getTag(),
v.getApiName(), v.getUid(), v.getApiResult(), v.getCount())));
+ mApiStatsMap.clear();
+ onAggregate();
return StatsManager.PULL_SUCCESS;
} else {
return StatsManager.PULL_SKIP;
diff --git a/src/com/android/server/telecom/metrics/AudioRouteStats.java b/src/com/android/server/telecom/metrics/AudioRouteStats.java
index 21624f1..4611b22 100644
--- a/src/com/android/server/telecom/metrics/AudioRouteStats.java
+++ b/src/com/android/server/telecom/metrics/AudioRouteStats.java
@@ -99,6 +99,8 @@
TelecomStatsLog.buildStatsEvent(getTag(),
v.getCallAudioRouteSource(), v.getCallAudioRouteDest(),
v.getSuccess(), v.getRevert(), v.getCount(), v.getAverageLatencyMs())));
+ mAudioRouteStatsMap.clear();
+ onAggregate();
return StatsManager.PULL_SUCCESS;
} else {
return StatsManager.PULL_SKIP;
diff --git a/src/com/android/server/telecom/metrics/CallStats.java b/src/com/android/server/telecom/metrics/CallStats.java
index 7ebeba6..8bdeffb 100644
--- a/src/com/android/server/telecom/metrics/CallStats.java
+++ b/src/com/android/server/telecom/metrics/CallStats.java
@@ -81,6 +81,8 @@
v.getCallDirection(), v.getExternalCall(), v.getEmergencyCall(),
v.getMultipleAudioAvailable(), v.getAccountType(), v.getUid(),
v.getCount(), v.getAverageDurationMs())));
+ mCallStatsMap.clear();
+ onAggregate();
return StatsManager.PULL_SUCCESS;
} else {
return StatsManager.PULL_SKIP;
diff --git a/src/com/android/server/telecom/metrics/ErrorStats.java b/src/com/android/server/telecom/metrics/ErrorStats.java
index f70f6d8..f334710 100644
--- a/src/com/android/server/telecom/metrics/ErrorStats.java
+++ b/src/com/android/server/telecom/metrics/ErrorStats.java
@@ -140,6 +140,8 @@
Arrays.stream(mPulledAtoms.telecomErrorStats).forEach(v -> data.add(
TelecomStatsLog.buildStatsEvent(getTag(),
v.getSubmodule(), v.getError(), v.getCount())));
+ mErrorStatsMap.clear();
+ onAggregate();
return StatsManager.PULL_SUCCESS;
} else {
return StatsManager.PULL_SKIP;
diff --git a/src/com/android/server/telecom/metrics/TelecomMetricsController.java b/src/com/android/server/telecom/metrics/TelecomMetricsController.java
index df735c0..c642303 100644
--- a/src/com/android/server/telecom/metrics/TelecomMetricsController.java
+++ b/src/com/android/server/telecom/metrics/TelecomMetricsController.java
@@ -24,6 +24,7 @@
import android.annotation.NonNull;
import android.app.StatsManager;
import android.content.Context;
+import android.os.Binder;
import android.os.HandlerThread;
import android.telecom.Log;
import android.util.StatsEvent;
@@ -73,8 +74,13 @@
public ApiStats getApiStats() {
ApiStats stats = (ApiStats) mStats.get(TELECOM_API_STATS);
if (stats == null) {
- stats = new ApiStats(mContext, mHandlerThread.getLooper());
- registerAtom(stats.getTag(), stats);
+ long token = Binder.clearCallingIdentity();
+ try {
+ stats = new ApiStats(mContext, mHandlerThread.getLooper());
+ registerAtom(stats.getTag(), stats);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
}
return stats;
}
diff --git a/src/com/android/server/telecom/ui/CallStreamingNotification.java b/src/com/android/server/telecom/ui/CallStreamingNotification.java
index 8414047..06da5e3 100644
--- a/src/com/android/server/telecom/ui/CallStreamingNotification.java
+++ b/src/com/android/server/telecom/ui/CallStreamingNotification.java
@@ -192,7 +192,7 @@
// Use the caller name for the label if available, default to app name if none.
if (TextUtils.isEmpty(callerName)) {
// App did not provide a caller name, so default to app's name.
- callerName = mAppLabelProxy.getAppLabel(appPackageName).toString();
+ callerName = mAppLabelProxy.getAppLabel(appPackageName, userHandle).toString();
}
// Action to hangup; this can use the default hangup action from the call style
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
index ac4a94e..9f97bbe 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
@@ -18,6 +18,9 @@
import static android.media.AudioDeviceInfo.TYPE_BUILTIN_SPEAKER;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE;
+import static com.android.server.telecom.CallAudioRouteController.INCLUDE_BLUETOOTH_IN_BASELINE;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
@@ -44,11 +47,14 @@
import android.media.AudioManager;
import android.os.Bundle;
import android.os.Parcel;
+import android.telecom.CallAudioState;
import androidx.test.filters.SmallTest;
+import com.android.server.telecom.AudioRoute;
import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
import com.android.server.telecom.CallAudioRouteAdapter;
+import com.android.server.telecom.CallAudioRouteController;
import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
@@ -64,7 +70,9 @@
import static org.mockito.Mockito.reset;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.Executor;
@RunWith(JUnit4.class)
@@ -79,6 +87,8 @@
@Mock AudioManager mockAudioManager;
@Mock AudioDeviceInfo mSpeakerInfo;
@Mock Executor mExecutor;
+ @Mock CallAudioRouteController mCallAudioRouteController;
+ @Mock CallAudioState mCallAudioState;
BluetoothDeviceManager mBluetoothDeviceManager;
BluetoothProfile.ServiceListener serviceListenerUnderTest;
@@ -115,6 +125,7 @@
mBluetoothDeviceManager = new BluetoothDeviceManager(mContext, mAdapter,
mCommunicationDeviceTracker, mFeatureFlags);
mBluetoothDeviceManager.setBluetoothRouteManager(mRouteManager);
+ mBluetoothDeviceManager.setCallAudioRouteAdapter(mCallAudioRouteController);
mCommunicationDeviceTracker.setBluetoothRouteManager(mRouteManager);
mockAudioManager = mContext.getSystemService(AudioManager.class);
@@ -299,6 +310,38 @@
@SmallTest
@Test
+ public void testHandleAudioRefactoringServiceDisconnectedWhileBluetooth() {
+ when(mFeatureFlags.skipBaselineSwitchWhenRouteNotBluetooth()).thenReturn(true);
+ Map<AudioRoute, BluetoothDevice> btRoutes = new HashMap<>();
+ when(mCallAudioRouteController.getBluetoothRoutes()).thenReturn(btRoutes);
+ when(mCallAudioRouteController.getCurrentCallAudioState()).thenReturn(mCallAudioState);
+ when(mCallAudioState.getRoute()).thenReturn(CallAudioState.ROUTE_BLUETOOTH);
+
+ mBluetoothDeviceManager
+ .handleAudioRefactoringServiceDisconnected(BluetoothProfile.LE_AUDIO);
+
+ verify(mCallAudioRouteController).sendMessageWithSessionInfo(SWITCH_BASELINE_ROUTE,
+ INCLUDE_BLUETOOTH_IN_BASELINE, (String) null);
+ }
+
+ @SmallTest
+ @Test
+ public void testHandleAudioRefactoringServiceDisconnectedWhileSpeaker() {
+ when(mFeatureFlags.skipBaselineSwitchWhenRouteNotBluetooth()).thenReturn(true);
+ Map<AudioRoute, BluetoothDevice> btRoutes = new HashMap<>();
+ when(mCallAudioRouteController.getBluetoothRoutes()).thenReturn(btRoutes);
+ when(mCallAudioRouteController.getCurrentCallAudioState()).thenReturn(mCallAudioState);
+ when(mCallAudioState.getRoute()).thenReturn(CallAudioState.ROUTE_SPEAKER);
+
+ mBluetoothDeviceManager
+ .handleAudioRefactoringServiceDisconnected(BluetoothProfile.LE_AUDIO);
+
+ verify(mCallAudioRouteController, never()).sendMessageWithSessionInfo(SWITCH_BASELINE_ROUTE,
+ INCLUDE_BLUETOOTH_IN_BASELINE, (String) null);
+ }
+
+ @SmallTest
+ @Test
public void testHeadsetServiceDisconnect() {
receiverUnderTest.onReceive(mContext,
buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device1,
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
index 7d614dc..e234d9d 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -57,6 +57,7 @@
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -75,6 +76,7 @@
import android.os.UserHandle;
import android.telecom.CallAudioState;
import android.telecom.VideoProfile;
+import android.util.Pair;
import androidx.test.filters.SmallTest;
@@ -730,6 +732,14 @@
@SmallTest
@Test
+ public void testConnectDisconnectScoDuringCallNoClear() {
+ when(mFeatureFlags.onlyClearCommunicationDeviceOnInactive()).thenReturn(true);
+ verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_SCO);
+ verifyDisconnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_SCO);
+ }
+
+ @SmallTest
+ @Test
public void testConnectAndDisconnectLeDeviceDuringCall() {
when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
.thenReturn(BLUETOOTH_DEVICE_1);
@@ -739,6 +749,16 @@
@SmallTest
@Test
+ public void testConnectAndDisconnectLeDeviceDuringCallNoClear() {
+ when(mFeatureFlags.onlyClearCommunicationDeviceOnInactive()).thenReturn(true);
+ when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
+ .thenReturn(BLUETOOTH_DEVICE_1);
+ verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+ verifyDisconnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+ }
+
+ @SmallTest
+ @Test
public void testConnectAndDisconnectHearingAidDuringCall() {
verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_HA);
verifyDisconnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_HA);
@@ -746,6 +766,15 @@
@SmallTest
@Test
+ public void testConnectAndDisconnectHearingAidDuringCallNoClear() {
+ when(mFeatureFlags.onlyClearCommunicationDeviceOnInactive()).thenReturn(true);
+ verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_HA);
+ verifyDisconnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_HA);
+ }
+
+
+ @SmallTest
+ @Test
public void testSwitchBetweenLeAndScoDevices() {
when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
.thenReturn(BLUETOOTH_DEVICE_1);
@@ -1113,6 +1142,63 @@
@Test
@SmallTest
+ public void testRouteToWatchWhenCallAnsweredOnWatch_MultipleBtDevices() {
+ when(mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()).thenReturn(true);
+ // Connect first BT device.
+ verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_SCO);
+ // Connect another BT device.
+ String scoDeviceAddress = "00:00:00:00:00:03";
+ BluetoothDevice watchDevice =
+ BluetoothRouteManagerTest.makeBluetoothDevice(scoDeviceAddress);
+ when(mBluetoothRouteManager.isWatch(eq(watchDevice))).thenReturn(true);
+ BLUETOOTH_DEVICES.add(watchDevice);
+
+ mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+ watchDevice);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER
+ | CallAudioState.ROUTE_BLUETOOTH, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Signal that watch is now the active device. This is done in BluetoothStateReceiver and
+ // then BT_ACTIVE_DEVICE_PRESENT will be sent to the controller to be processed.
+ mController.updateActiveBluetoothDevice(
+ new Pair<>(AudioRoute.TYPE_BLUETOOTH_SCO, watchDevice.getAddress()));
+ // Emulate scenario with call answered on watch. Ensure at this point that audio was routed
+ // into watch
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0);
+ mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED,
+ 0, watchDevice);
+ mController.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED,
+ 0, BLUETOOTH_DEVICE_1);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER
+ | CallAudioState.ROUTE_BLUETOOTH, watchDevice, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Hardcode signal from BT stack signaling to Telecom that watch is now the active device.
+ // This should just be a no-op since audio was already routed when processing active focus.
+ mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+ AudioRoute.TYPE_BLUETOOTH_SCO, scoDeviceAddress);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Mimic behavior of controller processing BT_AUDIO_DISCONNECTED for BLUETOOTH_DEVICE_1 and
+ // verify that audio remains routed to the watch and not routed to earpiece (this should
+ // be taking into account what the BT active device is as reported to us by the BT stack).
+ mController.sendMessageWithSessionInfo(SWITCH_BASELINE_ROUTE,
+ INCLUDE_BLUETOOTH_IN_BASELINE, BLUETOOTH_DEVICE_1.getAddress());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ BLUETOOTH_DEVICES.remove(watchDevice);
+ }
+
+
+ @Test
+ @SmallTest
public void testAbandonCallAudioFocusAfterCallEnd() {
// Make sure in-band ringing is disabled so that route never becomes active
when(mBluetoothRouteManager.isInbandRingEnabled(eq(BLUETOOTH_DEVICE_1))).thenReturn(false);
@@ -1193,7 +1279,17 @@
if (audioType == AudioRoute.TYPE_BLUETOOTH_SCO) {
verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT)).disconnectSco();
} else {
- verify(mAudioManager, timeout(TEST_TIMEOUT)).clearCommunicationDevice();
+ if (mFeatureFlags.onlyClearCommunicationDeviceOnInactive()) {
+ verify(mAudioManager, timeout(TEST_TIMEOUT).times(2))
+ .setCommunicationDevice(any(AudioDeviceInfo.class));
+ // Don't use a timeout here because that will cause the test to pause for a long
+ // period of time to verify; the previous verify has a timeout on it, so it will
+ // have already waited for any AudioManager invocations to take place. Any
+ // potential clear would have happened by now.
+ verify(mAudioManager, never()).clearCommunicationDevice();
+ } else {
+ verify(mAudioManager, timeout(TEST_TIMEOUT)).clearCommunicationDevice();
+ }
}
verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
any(CallAudioState.class), eq(expectedState));
diff --git a/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java b/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java
index d1427db..d97263d 100644
--- a/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java
@@ -17,6 +17,7 @@
package com.android.server.telecom.tests;
import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
@@ -136,7 +137,7 @@
when(mContext.getSystemService(TelecomManager.class))
.thenReturn(mTelecomManager);
when(mTelecomManager.getSystemDialerPackage()).thenReturn(PKG_NAME);
- when(mAppLabelProxy.getAppLabel(PKG_NAME)).thenReturn(APP_NAME);
+ when(mAppLabelProxy.getAppLabel(PKG_NAME, PA_HANDLE.getUserHandle())).thenReturn(APP_NAME);
when(mParcelableCallUtilsConverter.toParcelableCall(
eq(mCall), anyBoolean(), eq(mPhoneAccountRegistrar))).thenReturn(null);
when(mContext.bindServiceAsUser(nullable(Intent.class), nullable(ServiceConnection.class),
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index 79fd3d5..34d8830 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -3803,9 +3803,9 @@
Call callSpy = Mockito.spy(ongoingCall);
// Mocks some methods to not call the real method.
- doNothing().when(callSpy).unhold();
- doNothing().when(callSpy).hold();
- doNothing().when(callSpy).answer(ArgumentMatchers.anyInt());
+ doReturn(null).when(callSpy).unhold();
+ doReturn(null).when(callSpy).hold();
+ doReturn(null).when(callSpy).answer(ArgumentMatchers.anyInt());
doNothing().when(callSpy).setStartWithSpeakerphoneOn(ArgumentMatchers.anyBoolean());
mCallsManager.addCall(callSpy);
@@ -3817,10 +3817,10 @@
Call callSpy = Mockito.spy(ongoingCall);
// Mocks some methods to not call the real method.
- doNothing().when(callSpy).unhold();
- doNothing().when(callSpy).hold();
- doNothing().when(callSpy).disconnect();
- doNothing().when(callSpy).answer(ArgumentMatchers.anyInt());
+ doReturn(null).when(callSpy).unhold();
+ doReturn(null).when(callSpy).hold();
+ doReturn(null).when(callSpy).disconnect();
+ doReturn(null).when(callSpy).answer(ArgumentMatchers.anyInt());
doNothing().when(callSpy).setStartWithSpeakerphoneOn(ArgumentMatchers.anyBoolean());
return callSpy;
diff --git a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
index 519e596..b6c3743 100644
--- a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
+++ b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
@@ -113,8 +113,7 @@
private final String PACKAGE_1 = "PACKAGE_1";
private final String PACKAGE_2 = "PACKAGE_2";
private final String COMPONENT_NAME = "com.android.server.telecom.tests.MockConnectionService";
- private final UserHandle USER_HANDLE_10 = UserHandle.of(10);
- private final UserHandle USER_HANDLE_1000 = UserHandle.of(1000);
+ private final UserHandle USER_HANDLE_10 = new UserHandle(10);
private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
private PhoneAccountRegistrar mRegistrar;
@Mock private SubscriptionManager mSubscriptionManager;
@@ -136,12 +135,11 @@
.delete();
when(mDefaultDialerCache.getDefaultDialerApplication(anyInt()))
.thenReturn("com.android.dialer");
- when(mAppLabelProxy.getAppLabel(anyString()))
+ when(mAppLabelProxy.getAppLabel(anyString(), any()))
.thenReturn(TEST_LABEL);
mRegistrar = new PhoneAccountRegistrar(
mComponentContextFixture.getTestDouble().getApplicationContext(), mLock, FILE_NAME,
mDefaultDialerCache, mAppLabelProxy, mTelephonyFeatureFlags, mFeatureFlags);
- mRegistrar.setCurrentUserHandle(UserHandle.SYSTEM);
when(mFeatureFlags.onlyUpdateTelephonyOnValidSubIds()).thenReturn(false);
when(mFeatureFlags.unregisterUnresolvableAccounts()).thenReturn(true);
when(mTelephonyFeatureFlags.workProfileApiSplit()).thenReturn(false);
@@ -1308,7 +1306,8 @@
Mockito.mock(IConnectionService.class));
UserManager userManager = mContext.getSystemService(UserManager.class);
- List<UserHandle> users = Arrays.asList(UserHandle.SYSTEM, USER_HANDLE_1000);
+ List<UserHandle> users = Arrays.asList(new UserHandle(0),
+ new UserHandle(1000));
PhoneAccount pa1 = new PhoneAccount.Builder(
new PhoneAccountHandle(new ComponentName(PACKAGE_1, COMPONENT_NAME), "1234",
@@ -1607,7 +1606,7 @@
.setCapabilities(PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS);
// WHEN
- when(mAppLabelProxy.getAppLabel(anyString())).thenReturn(invalidLabel);
+ when(mAppLabelProxy.getAppLabel(anyString(), any())).thenReturn(invalidLabel);
// THEN
try {
diff --git a/tests/src/com/android/server/telecom/tests/RingerTest.java b/tests/src/com/android/server/telecom/tests/RingerTest.java
index c4d9678..46916fd 100644
--- a/tests/src/com/android/server/telecom/tests/RingerTest.java
+++ b/tests/src/com/android/server/telecom/tests/RingerTest.java
@@ -66,6 +66,7 @@
import androidx.test.filters.SmallTest;
+import com.android.server.telecom.AnomalyReporterAdapter;
import com.android.server.telecom.AsyncRingtonePlayer;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallState;
@@ -123,6 +124,7 @@
@Mock NotificationManager mockNotificationManager;
@Mock Ringer.AccessibilityManagerAdapter mockAccessibilityManagerAdapter;
@Mock private FeatureFlags mFeatureFlags;
+ @Mock private AnomalyReporterAdapter mAnomalyReporterAdapter;
@Spy Ringer.VibrationEffectProxy spyVibrationEffectProxy;
@@ -178,7 +180,7 @@
mRingerUnderTest = new Ringer(mockPlayerFactory, mContext, mockSystemSettingsUtil,
asyncRingtonePlayer, mockRingtoneFactory, mockVibrator, spyVibrationEffectProxy,
mockInCallController, mockNotificationManager, mockAccessibilityManagerAdapter,
- mFeatureFlags);
+ mFeatureFlags, mAnomalyReporterAdapter);
// This future is used to wait for AsyncRingtonePlayer to finish its part.
mRingerUnderTest.setBlockOnRingingFuture(mRingCompletionFuture);
}
diff --git a/tests/src/com/android/server/telecom/tests/TelecomPulledAtomTest.java b/tests/src/com/android/server/telecom/tests/TelecomPulledAtomTest.java
index 8ae734c..d3c7859 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomPulledAtomTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomPulledAtomTest.java
@@ -207,12 +207,14 @@
createTestFileForApiStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS - 1);
ApiStats apiStats = spy(new ApiStats(mSpyContext, mLooper));
final List<StatsEvent> data = new ArrayList<>();
+ int sizePulled = apiStats.mPulledAtoms.telecomApiStats.length;
int result = apiStats.pull(data);
assertEquals(StatsManager.PULL_SUCCESS, result);
verify(apiStats).onPull(eq(data));
- assertEquals(data.size(), apiStats.mPulledAtoms.telecomApiStats.length);
+ assertEquals(data.size(), sizePulled);
+ assertEquals(apiStats.mPulledAtoms.telecomApiStats.length, 0);
}
@Test
@@ -233,12 +235,14 @@
createTestFileForAudioRouteStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS - 1);
AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
final List<StatsEvent> data = new ArrayList<>();
+ int sizePulled = audioRouteStats.mPulledAtoms.callAudioRouteStats.length;
int result = audioRouteStats.pull(data);
assertEquals(StatsManager.PULL_SUCCESS, result);
verify(audioRouteStats).onPull(eq(data));
- assertEquals(data.size(), audioRouteStats.mPulledAtoms.callAudioRouteStats.length);
+ assertEquals(data.size(), sizePulled);
+ assertEquals(audioRouteStats.mPulledAtoms.callAudioRouteStats.length, 0);
}
@Test
@@ -259,12 +263,14 @@
createTestFileForCallStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS - 1);
CallStats callStats = spy(new CallStats(mSpyContext, mLooper));
final List<StatsEvent> data = new ArrayList<>();
+ int sizePulled = callStats.mPulledAtoms.callStats.length;
int result = callStats.pull(data);
assertEquals(StatsManager.PULL_SUCCESS, result);
verify(callStats).onPull(eq(data));
- assertEquals(data.size(), callStats.mPulledAtoms.callStats.length);
+ assertEquals(data.size(), sizePulled);
+ assertEquals(callStats.mPulledAtoms.callStats.length, 0);
}
@Test
@@ -285,12 +291,14 @@
createTestFileForErrorStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS - 1);
ErrorStats errorStats = spy(new ErrorStats(mSpyContext, mLooper));
final List<StatsEvent> data = new ArrayList<>();
+ int sizePulled = errorStats.mPulledAtoms.telecomErrorStats.length;
int result = errorStats.pull(data);
assertEquals(StatsManager.PULL_SUCCESS, result);
verify(errorStats).onPull(eq(data));
- assertEquals(data.size(), errorStats.mPulledAtoms.telecomErrorStats.length);
+ assertEquals(data.size(), sizePulled);
+ assertEquals(errorStats.mPulledAtoms.telecomErrorStats.length, 0);
}
@Test
diff --git a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
index 07a12c4..220d783 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
@@ -93,6 +93,7 @@
import com.android.server.telecom.PhoneAccountRegistrar;
import com.android.server.telecom.TelecomServiceImpl;
import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.callsequencing.CallTransaction;
import com.android.server.telecom.components.UserCallIntentProcessor;
import com.android.server.telecom.components.UserCallIntentProcessorFactory;
import com.android.server.telecom.flags.FeatureFlags;
@@ -116,6 +117,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.IntConsumer;
@@ -206,9 +208,12 @@
@Mock private InCallController mInCallController;
@Mock private TelecomMetricsController mMockTelecomMetricsController;
+ @Mock private OutgoingCallTransaction mOutgoingCallTransaction;
+ @Mock private IncomingCallTransaction mIncomingCallTransaction;
private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
+ private static final String SYSTEM_UI_PACKAGE = "com.android.systemui";
private static final String DEFAULT_DIALER_PACKAGE = "com.google.android.dialer";
private static final UserHandle USER_HANDLE_16 = new UserHandle(16);
private static final UserHandle USER_HANDLE_17 = new UserHandle(17);
@@ -260,7 +265,8 @@
mFeatureFlags,
mTelephonyFeatureFlags,
mLock,
- mMockTelecomMetricsController);
+ mMockTelecomMetricsController,
+ SYSTEM_UI_PACKAGE);
telecomServiceImpl.setTransactionManager(mTransactionManager);
telecomServiceImpl.setAnomalyReporterAdapter(mAnomalyReporterAdapter);
mTSIBinder = telecomServiceImpl.getBinder();
@@ -280,6 +286,7 @@
when(mPackageManager.getPackageUid(anyString(), eq(0))).thenReturn(Binder.getCallingUid());
when(mFeatureFlags.earlyBindingToIncallService()).thenReturn(true);
when(mTelephonyFeatureFlags.workProfileApiSplit()).thenReturn(false);
+ when(mFeatureFlags.enableCallSequencing()).thenReturn(false);
}
@Override
@@ -455,6 +462,9 @@
// WHEN
when(mFakePhoneAccountRegistrar.getPhoneAccountUnchecked(TEL_PA_HANDLE_CURRENT)).thenReturn(
phoneAccount);
+ when(mFakeCallsManager.createTransactionalCall(any(String.class),
+ any(CallAttributes.class), any(Bundle.class), any(String.class)))
+ .thenReturn(CompletableFuture.completedFuture(mOutgoingCallTransaction));
doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount(
eq(TEL_PA_HANDLE_CURRENT), any(UserHandle.class));
@@ -483,6 +493,9 @@
doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount(
eq(TEL_PA_HANDLE_CURRENT), any(UserHandle.class));
+ when(mFakeCallsManager.createTransactionalCall(any(String.class),
+ any(CallAttributes.class), any(Bundle.class), any(String.class)))
+ .thenReturn(CompletableFuture.completedFuture(mIncomingCallTransaction));
mTSIBinder.addCall(mIncomingCallAttributes, mICallEventCallback, "1", CALLING_PACKAGE);
@@ -2307,7 +2320,8 @@
}
/**
- * Ensure self-managed calls cannot be ended using {@link TelecomManager#endCall()}.
+ * Ensure self-managed calls cannot be ended using {@link TelecomManager#endCall()} when the
+ * caller of this method is not considered privileged.
* @throws Exception
*/
@SmallTest
@@ -2324,7 +2338,8 @@
/**
* Ensure self-managed calls cannot be answered using {@link TelecomManager#acceptRingingCall()}
- * or {@link TelecomManager#acceptRingingCall(int)}.
+ * or {@link TelecomManager#acceptRingingCall(int)} when the caller of these methods is not
+ * considered privileged.
* @throws Exception
*/
@SmallTest
@@ -2339,6 +2354,53 @@
verify(mFakeCallsManager, never()).answerCall(eq(call), anyInt());
}
+ /**
+ * Ensure self-managed calls can be answered using {@link TelecomManager#acceptRingingCall()}
+ * or {@link TelecomManager#acceptRingingCall(int)} if the caller of these methods is
+ * privileged.
+ * @throws Exception
+ */
+ @SmallTest
+ @Test
+ public void testCanAnswerSelfManagedCallIfPrivileged() throws Exception {
+ when(mFeatureFlags.allowSystemAppsResolveVoipCalls()).thenReturn(true);
+ // Configure the test so that the caller of acceptRingingCall is considered privileged:
+ when(mPackageManager.getPackageUid(SYSTEM_UI_PACKAGE, 0))
+ .thenReturn(Binder.getCallingUid());
+
+ // Ensure that the call is successfully accepted:
+ Call call = mock(Call.class);
+ when(call.isSelfManaged()).thenReturn(true);
+ when(call.getState()).thenReturn(CallState.ACTIVE);
+ when(mFakeCallsManager.getFirstCallWithState(any()))
+ .thenReturn(call);
+ mTSIBinder.acceptRingingCall(TEST_PACKAGE);
+ verify(mFakeCallsManager).answerCall(eq(call), anyInt());
+ }
+
+ /**
+ * Ensure self-managed calls can be ended using {@link TelecomManager#endCall()} when the
+ * caller of these methods is privileged.
+ * @throws Exception
+ */
+ @SmallTest
+ @Test
+ public void testCanEndSelfManagedCallIfPrivileged() throws Exception {
+ when(mFeatureFlags.allowSystemAppsResolveVoipCalls()).thenReturn(true);
+ // Configure the test so that the caller of endCall is considered privileged:
+ when(mPackageManager.getPackageUid(SYSTEM_UI_PACKAGE, 0))
+ .thenReturn(Binder.getCallingUid());
+ // Set up the call:
+ Call call = mock(Call.class);
+ when(call.isSelfManaged()).thenReturn(true);
+ when(call.getState()).thenReturn(CallState.ACTIVE);
+ when(mFakeCallsManager.getFirstCallWithState(any()))
+ .thenReturn(call);
+ // Ensure that the call is successfully ended:
+ assertTrue(mTSIBinder.endCall(TEST_PACKAGE));
+ verify(mFakeCallsManager).disconnectCall(eq(call));
+ }
+
@SmallTest
@Test
public void testGetAdnUriForPhoneAccount() throws Exception {
diff --git a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
index 4463d65..1e65011 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
@@ -224,6 +224,7 @@
@Mock
com.android.internal.telephony.flags.FeatureFlags mTelephonyFlags;
+ private static final String SYSTEM_UI_PACKAGE = "com.android.systemui";
final ComponentName mInCallServiceComponentNameX =
new ComponentName(
"incall-service-package-X",
@@ -580,7 +581,8 @@
ContactsAsyncHelper.ContentResolverAdapter adapter) {
return new ContactsAsyncHelper(adapter, mHandlerThread.getLooper());
}
- }, mDeviceIdleControllerAdapter, mAccessibilityManagerAdapter,
+ }, mDeviceIdleControllerAdapter, SYSTEM_UI_PACKAGE,
+ mAccessibilityManagerAdapter,
Runnable::run,
Runnable::run,
mBlockedNumbersAdapter,
diff --git a/tests/src/com/android/server/telecom/tests/TransactionTests.java b/tests/src/com/android/server/telecom/tests/TransactionTests.java
index 78c2210..0a23913 100644
--- a/tests/src/com/android/server/telecom/tests/TransactionTests.java
+++ b/tests/src/com/android/server/telecom/tests/TransactionTests.java
@@ -292,20 +292,21 @@
.setCallType(CallAttributes.VIDEO_CALL)
.build();
+ Bundle extras = new Bundle();
OutgoingCallTransaction t = new OutgoingCallTransaction(null,
- mContext, null, mCallsManager, new Bundle(), mFeatureFlags);
+ mContext, null, mCallsManager, extras, mFeatureFlags);
// WHEN
when(mFeatureFlags.transactionalVideoState()).thenReturn(true);
t.setFeatureFlags(mFeatureFlags);
// THEN
- assertEquals(VideoProfile.STATE_AUDIO_ONLY, t
- .generateExtras(audioOnlyAttributes)
+ assertEquals(VideoProfile.STATE_AUDIO_ONLY, OutgoingCallTransaction
+ .generateExtras(null, extras, audioOnlyAttributes, mFeatureFlags)
.getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE));
- assertEquals(VideoProfile.STATE_BIDIRECTIONAL, t
- .generateExtras(videoAttributes)
+ assertEquals(VideoProfile.STATE_BIDIRECTIONAL, OutgoingCallTransaction
+ .generateExtras(null, extras, videoAttributes, mFeatureFlags)
.getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE));
}
@@ -448,9 +449,9 @@
callSpy.setState(initialState, "manual set in test");
// Mocks some methods to not call the real method.
- doNothing().when(callSpy).unhold();
- doNothing().when(callSpy).hold();
- doNothing().when(callSpy).disconnect();
+ doReturn(null).when(callSpy).unhold();
+ doReturn(null).when(callSpy).hold();
+ doReturn(null).when(callSpy).disconnect();
return callSpy;
}