Merge "Import translations. DO NOT MERGE ANYWHERE"
diff --git a/Android.bp b/Android.bp
index c5141ca..501b438 100644
--- a/Android.bp
+++ b/Android.bp
@@ -27,6 +27,7 @@
],
static_libs: [
"androidx.annotation_annotation",
+ "androidx.core_core",
],
libs: [
"services",
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index d42dcff..ab067d9 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -63,6 +63,8 @@
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS"/>
<uses-permission android:name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
+ <uses-permission android:name="android.permission.USE_COLORIZED_NOTIFICATIONS"/>
+ <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
<uses-permission android:name="com.android.phone.permission.ACCESS_LAST_KNOWN_CELL_ID"/>
<uses-permission android:name="android.permission.STATUS_BAR_SERVICE" />
diff --git a/res/drawable/gm_phonelink.xml b/res/drawable/gm_phonelink.xml
new file mode 100644
index 0000000..2ffba0e
--- /dev/null
+++ b/res/drawable/gm_phonelink.xml
@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?android:attr/colorControlNormal"
+ android:autoMirrored="true">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M5,6h16L21,4L5,4c-1.1,0 -2,0.9 -2,2v11L1,17v3h11v-3L5,17L5,6zM21,8h-6c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h6c0.55,0 1,-0.45 1,-1L22,9c0,-0.55 -0.45,-1 -1,-1zM20,17h-4v-7h4v7z"/>
+</vector>
diff --git a/res/drawable/person_circle.xml b/res/drawable/person_circle.xml
new file mode 100644
index 0000000..e139b4f
--- /dev/null
+++ b/res/drawable/person_circle.xml
@@ -0,0 +1,13 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M21.094,0.92C22.839,-0.307 25.161,-0.307 26.906,0.92C28.031,1.71 29.428,2.009 30.777,1.746C32.868,1.339 34.989,2.287 36.086,4.12C36.794,5.302 37.95,6.145 39.288,6.456C41.363,6.938 42.917,8.671 43.177,10.794C43.345,12.163 44.059,13.406 45.156,14.236C46.856,15.524 47.574,17.742 46.952,19.788C46.551,21.107 46.7,22.534 47.365,23.741C48.397,25.612 48.154,27.931 46.758,29.546C45.857,30.587 45.416,31.951 45.535,33.326C45.72,35.457 44.559,37.476 42.629,38.381C41.384,38.965 40.429,40.03 39.981,41.335C39.287,43.357 37.408,44.727 35.279,44.766C33.906,44.79 32.601,45.374 31.664,46.382C30.211,47.946 27.939,48.431 25.979,47.596C24.714,47.057 23.286,47.057 22.021,47.596C20.061,48.431 17.789,47.946 16.336,46.382C15.399,45.374 14.094,44.79 12.721,44.766C10.592,44.727 8.713,43.357 8.019,41.335C7.571,40.03 6.616,38.965 5.371,38.381C3.441,37.476 2.28,35.457 2.465,33.326C2.584,31.951 2.143,30.587 1.242,29.546C-0.154,27.931 -0.397,25.612 0.635,23.741C1.3,22.534 1.449,21.107 1.048,19.788C0.427,17.742 1.144,15.524 2.844,14.236C3.941,13.406 4.655,12.163 4.823,10.794C5.083,8.671 6.637,6.938 8.712,6.456C10.05,6.145 11.206,5.302 11.914,4.12C13.011,2.287 15.132,1.339 17.223,1.746C18.572,2.009 19.969,1.71 21.094,0.92Z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M24.001,15.467C21.644,15.467 19.734,17.376 19.734,19.733C19.734,22.091 21.644,24 24.001,24C26.358,24 28.268,22.091 28.268,19.733C28.268,17.376 26.358,15.467 24.001,15.467ZM26.134,19.734C26.134,18.56 25.174,17.601 24,17.601C22.827,17.601 21.867,18.56 21.867,19.734C21.867,20.907 22.827,21.867 24,21.867C25.174,21.867 26.134,20.907 26.134,19.734ZM30.402,29.333C30.188,28.576 26.882,27.2 24.002,27.2C21.122,27.2 17.815,28.576 17.602,29.344V30.4H30.402V29.333ZM15.469,29.333C15.469,26.496 21.154,25.066 24.002,25.066C26.85,25.066 32.535,26.496 32.535,29.333V32.533H15.469V29.333Z"
+ android:fillColor="#ffffff"
+ android:fillType="evenOdd"/>
+</vector>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index d67df4b..ec278f0 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -321,6 +321,10 @@
<string name="notification_channel_disconnected_calls">Disconnected calls</string>
<!-- Notification channel name for a channel containing crashed phone apps service notifications. -->
<string name="notification_channel_in_call_service_crash">Crashed phone apps</string>
+ <!-- Notification channel name for a channel containing notifications related to call streaming.
+ Call streaming is a feature where an app can use another device like a tablet to see and
+ control a call taking place on their phone. -->
+ <string name="notification_channel_call_streaming">Call streaming</string>
<!-- Alert dialog content used to inform the user that placing a new outgoing call will end the
ongoing call in the app "other_app". -->
@@ -395,4 +399,20 @@
<string name="callendpoint_name_streaming">External</string>
<!-- The user-visible name of the unknown new type CallEndpoint -->
<string name="callendpoint_name_unknown">Unknown</string>
+
+ <!-- The content of a notification shown when a call is being streamed to another device.
+ Call streaming is a feature where a user can see and interact with a call from another
+ device like a tablet while the call takes place on their phone. -->
+ <string name="call_streaming_notification_body">Streaming audio to other device</string>
+ <!-- A notification action which is shown when a call is being streamed to another device.
+ Tapping the action will hang up the call.
+ Call streaming is a feature where a user can see and interact with a call from another
+ device like a tablet while the call takes place on their phone. -->
+ <string name="call_streaming_notification_action_hang_up">Hang up</string>
+ <!-- A notification action which is shown when a call is being streamed to another device.
+ Tapping the action will move the call back to the phone from the device it is being
+ streamed to.
+ Call streaming is a feature where a user can see and interact with a call from another
+ device like a tablet while the call takes place on their phone. -->
+ <string name="call_streaming_notification_action_switch_here">Switch here</string>
</resources>
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 6ae8834..7b0b697 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -534,6 +534,7 @@
private boolean mWasHighDefAudio = false;
private boolean mWasWifi = false;
private boolean mWasVolte = false;
+ private boolean mDestroyed = false;
// For conferences which support merge/swap at their level, we retain a notion of an active
// call. This is used for BluetoothPhoneService. In order to support hold/merge, it must have
@@ -929,6 +930,9 @@
}
public void destroy() {
+ if (mDestroyed) {
+ return;
+ }
// We should not keep these bitmaps around because the Call objects may be held for logging
// purposes.
// TODO: Make a container object that only stores the information we care about for Logging.
@@ -939,6 +943,7 @@
closeRttStreams();
Log.addEvent(this, LogUtils.Events.DESTROYED);
+ mDestroyed = true;
}
private void closeRttStreams() {
@@ -999,6 +1004,9 @@
s.append(SimpleDateFormat.getDateTimeInstance().format(new Date(getCreationTimeMillis())));
s.append("]");
s.append(isIncoming() ? "(MT - incoming)" : "(MO - outgoing)");
+ s.append("(User=");
+ s.append(getInitiatingUser());
+ s.append(")");
s.append("\n\t");
PhoneAccountHandle targetPhoneAccountHandle = getTargetPhoneAccount();
@@ -4116,6 +4124,15 @@
* @param extras The extras.
*/
public void onConnectionEvent(String event, Bundle extras) {
+ if (mIsTransactionalCall) {
+ // send the Event directly to the ICS via the InCallController listener
+ for (Listener l : mListeners) {
+ l.onConnectionEvent(this, event, extras);
+ }
+ // Don't run the below block since it applies to Calls that are attached to a
+ // ConnectionService
+ return;
+ }
// Don't log call quality reports; they're quite frequent and will clog the log.
if (!Connection.EVENT_CALL_QUALITY_REPORT.equals(event)) {
Log.addEvent(this, LogUtils.Events.CONNECTION_EVENT, event);
diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java
index 38e6b00..f52a966 100644
--- a/src/com/android/server/telecom/CallAudioManager.java
+++ b/src/com/android/server/telecom/CallAudioManager.java
@@ -795,7 +795,7 @@
.setHasHoldingCalls(mHoldingCalls.size() > 0)
.setHasAudioProcessingCalls(mAudioProcessingCalls.size() > 0)
.setIsTonePlaying(mIsTonePlaying)
- .setIsStreaming(mStreamingCall != null)
+ .setIsStreaming((mStreamingCall != null) && (!mStreamingCall.isDisconnected()))
.setForegroundCallIsVoip(
mForegroundCall != null && isCallVoip(mForegroundCall))
.setSession(Log.createSubsession()).build();
diff --git a/src/com/android/server/telecom/CallStreamingController.java b/src/com/android/server/telecom/CallStreamingController.java
index 6276a7d..d90524d 100644
--- a/src/com/android/server/telecom/CallStreamingController.java
+++ b/src/com/android/server/telecom/CallStreamingController.java
@@ -87,6 +87,14 @@
mStreamingCall = null;
mTransactionalServiceWrapper = null;
if (mConnection != null) {
+ // Notify service streaming stopped and then unbind.
+ try {
+ mService.onCallStreamingStopped();
+ } catch (RemoteException e) {
+ // Could not notify stop streaming; we're about to just unbind so this is
+ // unfortunate but not the end of the world.
+ Log.e(this, e, "resetController: failed to notify stop streaming.");
+ }
mContext.unbindService(mConnection);
mConnection = null;
}
@@ -140,7 +148,7 @@
@Override
public CompletableFuture<VoipCallTransactionResult> processTransaction(Void v) {
- Log.d(this, "processTransaction");
+ Log.i(this, "processTransaction");
CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
if (mEnterInterception) {
@@ -178,9 +186,8 @@
@SuppressLint("LongLogTag")
@Override
public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
- Log.d(this, "processTransaction");
+ Log.i(this, "processTransaction");
CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
-
RoleManager roleManager = mContext.getSystemService(RoleManager.class);
PackageManager packageManager = mContext.getPackageManager();
if (roleManager == null || packageManager == null) {
@@ -198,7 +205,7 @@
VoipCallTransactionResult.RESULT_FAILED, MESSAGE));
return future;
}
-
+ Log.i(this, "processTransaction: servicePackage=%s", holders.get(0));
Intent serviceIntent = new Intent(CallStreamingService.SERVICE_INTERFACE);
serviceIntent.setPackage(holders.get(0));
List<ResolveInfo> infos = packageManager.queryIntentServicesAsUser(serviceIntent,
@@ -223,7 +230,7 @@
Intent intent = new Intent(CallStreamingService.SERVICE_INTERFACE);
intent.setComponent(serviceInfo.getComponentName());
- mConnection = new CallStreamingServiceConnection(mCall, mWrapper, future);
+ mConnection = new CallStreamingServiceConnection(mCall, mWrapper, future);
if (!mContext.bindServiceAsUser(intent, mConnection, Context.BIND_AUTO_CREATE
| Context.BIND_FOREGROUND_SERVICE
| Context.BIND_SCHEDULE_LIKE_TOP_APP, mUserHandle)) {
@@ -232,7 +239,6 @@
VoipCallTransactionResult.RESULT_FAILED,
"STREAMING_FAILED_SENDER_BINDING_ERROR"));
}
-
return future;
}
}
@@ -249,7 +255,7 @@
@SuppressLint("LongLogTag")
@Override
public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
- Log.d(this, "processTransaction");
+ Log.i(this, "processTransaction (unbindStreaming");
CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
resetController();
@@ -280,11 +286,13 @@
case CallState.ON_HOLD:
transaction = new CallStreamingStateChangeTransaction(
StreamingCall.STATE_HOLDING);
+ break;
case CallState.DISCONNECTING:
case CallState.DISCONNECTED:
Log.addEvent(call, LogUtils.Events.STOP_STREAMING);
transaction = new CallStreamingStateChangeTransaction(
StreamingCall.STATE_DISCONNECTED);
+ break;
default:
// ignore
}
@@ -374,13 +382,6 @@
}
private void clearBinding() {
- try {
- if (mService != null) {
- mService.onCallStreamingStopped();
- }
- } catch (RemoteException e) {
- Log.e(this, e, "Exception when stop call streaming");
- }
resetController();
if (!mFuture.isDone()) {
mFuture.complete(new VoipCallTransactionResult(
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 555ac42..dde7f0c 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -41,10 +41,12 @@
import android.Manifest;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.app.KeyguardManager;
import android.app.NotificationManager;
+import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
@@ -53,6 +55,7 @@
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.ResolveInfoFlags;
+import android.content.pm.ResolveInfo;
import android.content.pm.UserInfo;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
@@ -77,7 +80,6 @@
import android.provider.BlockedNumberContract;
import android.provider.BlockedNumberContract.SystemContract;
import android.provider.CallLog.Calls;
-import android.provider.DeviceConfig;
import android.provider.Settings;
import android.sysprop.TelephonyProperties;
import android.telecom.CallAttributes;
@@ -113,6 +115,7 @@
import android.widget.Button;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.IntentForwarderActivity;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
@@ -132,6 +135,7 @@
import com.android.server.telecom.stats.CallFailureCause;
import com.android.server.telecom.ui.AudioProcessingNotification;
import com.android.server.telecom.ui.CallRedirectionTimeoutDialogActivity;
+import com.android.server.telecom.ui.CallStreamingNotification;
import com.android.server.telecom.ui.ConfirmCallDialogActivity;
import com.android.server.telecom.ui.DisconnectedCallNotifier;
import com.android.server.telecom.ui.IncomingCallNotifier;
@@ -448,6 +452,7 @@
private final BlockedNumbersAdapter mBlockedNumbersAdapter;
private final TransactionManager mTransactionManager;
private final UserManager mUserManager;
+ private final CallStreamingNotification mCallStreamingNotification;
private final ConnectionServiceFocusManager.CallsManagerRequester mRequester =
new ConnectionServiceFocusManager.CallsManagerRequester() {
@@ -560,7 +565,9 @@
BlockedNumbersAdapter blockedNumbersAdapter,
TransactionManager transactionManager,
EmergencyCallDiagnosticLogger emergencyCallDiagnosticLogger,
- CallAudioCommunicationDeviceTracker communicationDeviceTracker) {
+ CallAudioCommunicationDeviceTracker communicationDeviceTracker,
+ CallStreamingNotification callStreamingNotification) {
+
mContext = context;
mLock = lock;
mPhoneNumberUtilsAdapter = phoneNumberUtilsAdapter;
@@ -649,6 +656,7 @@
mTransactionManager = transactionManager;
mBlockedNumbersAdapter = blockedNumbersAdapter;
mCallStreamingController = new CallStreamingController(mContext, mLock);
+ mCallStreamingNotification = callStreamingNotification;
mListeners.add(mInCallWakeLockController);
mListeners.add(statusBarNotifier);
@@ -670,6 +678,7 @@
// this needs to be after the mCallAudioManager
mListeners.add(mPhoneStateBroadcaster);
mListeners.add(mVoipCallMonitor);
+ mListeners.add(mCallStreamingNotification);
mVoipCallMonitor.startMonitor();
@@ -1415,6 +1424,15 @@
extras.getInt(CallAttributes.CALL_CAPABILITIES_KEY,
CallAttributes.SUPPORTS_SET_INACTIVE), true);
call.setTargetPhoneAccount(phoneAccountHandle);
+ if (extras.containsKey(CallAttributes.DISPLAY_NAME_KEY)) {
+ CharSequence displayName = extras.getCharSequence(CallAttributes.DISPLAY_NAME_KEY);
+ if (!TextUtils.isEmpty(displayName)) {
+ call.setCallerDisplayName(displayName.toString(),
+ TelecomManager.PRESENTATION_ALLOWED);
+ }
+ }
+ // Incoming address was set via EXTRA_INCOMING_CALL_ADDRESS above.
+ call.setInitiatingUser(phoneAccountHandle.getUserHandle());
}
// Ensure new calls related to self-managed calls/connections are set as such. This will
@@ -1699,7 +1717,6 @@
boolean isReusedCall;
Uri handle = isConference ? Uri.parse("tel:conf-factory") : participants.get(0);
Call call = reuseOutgoingCall(handle);
-
PhoneAccount account =
mPhoneAccountRegistrar.getPhoneAccount(requestedAccountHandle, initiatingUser);
Bundle phoneAccountExtra = account != null ? account.getExtras() : null;
@@ -1740,6 +1757,14 @@
call.setConnectionCapabilities(
extras.getInt(CallAttributes.CALL_CAPABILITIES_KEY,
CallAttributes.SUPPORTS_SET_INACTIVE), true);
+ if (extras.containsKey(CallAttributes.DISPLAY_NAME_KEY)) {
+ CharSequence displayName = extras.getCharSequence(
+ CallAttributes.DISPLAY_NAME_KEY);
+ if (!TextUtils.isEmpty(displayName)) {
+ call.setCallerDisplayName(displayName.toString(),
+ TelecomManager.PRESENTATION_ALLOWED);
+ }
+ }
call.setTargetPhoneAccount(requestedAccountHandle);
}
@@ -1975,23 +2000,21 @@
return CompletableFuture.completedFuture(null);
}
if (accountSuggestions == null || accountSuggestions.isEmpty()) {
- if (isSwitchToManagedProfileDialogFlagEnabled()) {
- Uri callUri = callToPlace.getHandle();
- if (PhoneAccount.SCHEME_TEL.equals(callUri.getScheme())) {
- int managedProfileUserId = getManagedProfileUserId(mContext,
- initiatingUser.getIdentifier());
- if (managedProfileUserId != UserHandle.USER_NULL
- &&
- mPhoneAccountRegistrar.getCallCapablePhoneAccounts(
- handle.getScheme(), false,
- UserHandle.of(managedProfileUserId),
- false).size()
- != 0) {
- boolean dialogShown = showSwitchToManagedProfileDialog(
- callUri, initiatingUser, managedProfileUserId);
- if (dialogShown) {
- return CompletableFuture.completedFuture(null);
- }
+ Uri callUri = callToPlace.getHandle();
+ if (PhoneAccount.SCHEME_TEL.equals(callUri.getScheme())) {
+ int managedProfileUserId = getManagedProfileUserId(mContext,
+ initiatingUser.getIdentifier());
+ if (managedProfileUserId != UserHandle.USER_NULL
+ &&
+ mPhoneAccountRegistrar.getCallCapablePhoneAccounts(
+ handle.getScheme(), false,
+ UserHandle.of(managedProfileUserId),
+ false).size()
+ != 0) {
+ boolean dialogShown = showSwitchToManagedProfileDialog(
+ callUri, initiatingUser, managedProfileUserId);
+ if (dialogShown) {
+ return CompletableFuture.completedFuture(null);
}
}
}
@@ -2154,30 +2177,97 @@
return UserHandle.USER_NULL;
}
- private boolean isSwitchToManagedProfileDialogFlagEnabled() {
- return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_DEVICE_POLICY_MANAGER,
- "enable_switch_to_managed_profile_dialog", false);
- }
-
private boolean showSwitchToManagedProfileDialog(Uri callUri, UserHandle initiatingUser,
int managedProfileUserId) {
- try {
- Intent showErrorIntent = new Intent(
- TelecomManager.ACTION_SHOW_SWITCH_TO_WORK_PROFILE_FOR_CALL_DIALOG, callUri);
- showErrorIntent.addCategory(Intent.CATEGORY_DEFAULT);
- showErrorIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- showErrorIntent.putExtra(TelecomManager.EXTRA_MANAGED_PROFILE_USER_ID,
- managedProfileUserId);
-
- if (mContext.getPackageManager().queryIntentActivitiesAsUser(showErrorIntent,
- ResolveInfoFlags.of(0), initiatingUser).size() != 0) {
- mContext.startActivityAsUser(showErrorIntent, initiatingUser);
- return true;
- }
- } catch (Exception e) {
- Log.w(this, "Failed to launch switch to managed profile dialog");
+ // Note that the ACTION_CALL intent will resolve to Telecomm's UserCallActivity
+ // even if there is no dialer. Hence we explicitly check for whether a default dialer
+ // exists instead of relying on ActivityNotFound when sending the call intent.
+ if (TextUtils.isEmpty(
+ mDefaultDialerCache.getDefaultDialerApplication(managedProfileUserId))) {
+ Log.i(
+ this,
+ "Work profile telephony: default dialer app missing, showing error dialog.");
+ return maybeShowErrorDialog(callUri, managedProfileUserId, initiatingUser);
}
- return false;
+
+ UserManager userManager = mContext.getSystemService(UserManager.class);
+ if (userManager.isQuietModeEnabled(UserHandle.of(managedProfileUserId))) {
+ Log.i(
+ this,
+ "Work profile telephony: quiet mode enabled, showing error dialog");
+ return maybeShowErrorDialog(callUri, managedProfileUserId, initiatingUser);
+ }
+ Log.i(
+ this,
+ "Work profile telephony: show forwarding call to managed profile dialog");
+ return maybeRedirectToIntentForwarder(callUri, initiatingUser);
+ }
+
+ private boolean maybeRedirectToIntentForwarder(
+ Uri callUri,
+ UserHandle initiatingUser) {
+ // Note: This intent is selected to match the CALL_MANAGED_PROFILE filter in
+ // DefaultCrossProfileIntentFiltersUtils. This ensures that it is redirected to
+ // IntentForwarderActivity.
+ Intent forwardCallIntent = new Intent(Intent.ACTION_CALL, callUri);
+ forwardCallIntent.addCategory(Intent.CATEGORY_DEFAULT);
+ ResolveInfo resolveInfos =
+ mContext.getPackageManager()
+ .resolveActivityAsUser(
+ forwardCallIntent,
+ ResolveInfoFlags.of(0),
+ initiatingUser.getIdentifier());
+ // Check that the intent will actually open the resolver rather than looping to the personal
+ // profile. This should not happen due to the cross profile intent filters.
+ if (resolveInfos == null
+ || !resolveInfos
+ .getComponentInfo()
+ .getComponentName()
+ .getShortClassName()
+ .equals(IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE)) {
+ Log.w(
+ this,
+ "Work profile telephony: Intent would not resolve to forwarder activity.");
+ return false;
+ }
+
+ try {
+ mContext.startActivityAsUser(forwardCallIntent, initiatingUser);
+ return true;
+ } catch (ActivityNotFoundException e) {
+ Log.e(this, e, "Unable to start call intent for work telephony");
+ return false;
+ }
+ }
+
+ private boolean maybeShowErrorDialog(
+ Uri callUri,
+ int managedProfileUserId,
+ UserHandle initiatingUser) {
+ Intent showErrorIntent =
+ new Intent(
+ TelecomManager.ACTION_SHOW_SWITCH_TO_WORK_PROFILE_FOR_CALL_DIALOG,
+ callUri);
+ showErrorIntent.addCategory(Intent.CATEGORY_DEFAULT);
+ showErrorIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ showErrorIntent.putExtra(
+ TelecomManager.EXTRA_MANAGED_PROFILE_USER_ID, managedProfileUserId);
+ if (mContext.getPackageManager()
+ .queryIntentActivitiesAsUser(
+ showErrorIntent,
+ ResolveInfoFlags.of(0),
+ initiatingUser)
+ .isEmpty()) {
+ return false;
+ }
+ try {
+ mContext.startActivityAsUser(showErrorIntent, initiatingUser);
+ return true;
+ } catch (ActivityNotFoundException e) {
+ Log.e(
+ this, e,"Work profile telephony: Unable to show error dialog");
+ return false;
+ }
}
public void startConference(List<Uri> participants, Bundle clientExtras, String callingPackage,
@@ -6366,4 +6456,30 @@
public CallStreamingController getCallStreamingController() {
return mCallStreamingController;
}
+
+ /**
+ * Given a call identified by call id, get the instance from the list of calls.
+ * @param callId the call id.
+ * @return the call, or null if not found.
+ */
+ public @Nullable Call getCall(@NonNull String callId) {
+ Optional<Call> foundCall = mCalls.stream().filter(
+ c -> c.getId().equals(callId)).findFirst();
+ if (foundCall.isPresent()) {
+ return foundCall.get();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Triggers stopping of call streaming for a call by launching a stop streaming transaction.
+ * @param call the call.
+ */
+ public void stopCallStreaming(@NonNull Call call) {
+ if (call.getTransactionServiceWrapper() == null) {
+ return;
+ }
+ call.getTransactionServiceWrapper().stopCallStreaming(call);
+ }
}
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 5b727ab..c8625b0 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -504,6 +504,8 @@
Log.startSession(sessionInfo, LogUtils.Sessions.CSW_ADD_CONFERENCE_CALL,
mPackageAbbreviation);
+ if (ConnectionServiceWrapper.this.mIsRemoteConnectionService) return;
+
if (parcelableConference.getConnectElapsedTimeMillis() != 0
&& mContext.checkCallingOrSelfPermission(MODIFY_PHONE_STATE)
!= PackageManager.PERMISSION_GRANTED) {
@@ -911,6 +913,9 @@
public void addExistingConnection(String callId, ParcelableConnection connection,
Session.Info sessionInfo) {
Log.startSession(sessionInfo, "CSW.aEC", mPackageAbbreviation);
+
+ if (ConnectionServiceWrapper.this.mIsRemoteConnectionService) return;
+
UserHandle userHandle = Binder.getCallingUserHandle();
// Check that the Calling Package matches PhoneAccountHandle's Component Package
PhoneAccountHandle callingPhoneAccountHandle = connection.getPhoneAccount();
@@ -1319,6 +1324,7 @@
private final CallsManager mCallsManager;
private final AppOpsManager mAppOpsManager;
private final Context mContext;
+ public boolean mIsRemoteConnectionService = false;
private ConnectionServiceFocusManager.ConnectionServiceFocusListener mConnSvrFocusListener;
@@ -2475,13 +2481,13 @@
private void logIncoming(String msg, Object... params) {
// Keep these as debug; the incoming logging is traced on a package level through the
// session logging.
- Log.d(this, "CS -> TC[" + Log.getPackageAbbreviation(mComponentName) + "]: "
- + msg, params);
+ Log.i(this, "CS -> TC[" + Log.getPackageAbbreviation(mComponentName) + "]:"
+ + " isRCS = " + this.mIsRemoteConnectionService + ": " + msg, params);
}
private void logOutgoing(String msg, Object... params) {
- Log.d(this, "TC -> CS[" + Log.getPackageAbbreviation(mComponentName) + "]: "
- + msg, params);
+ Log.i(this, "TC -> CS[" + Log.getPackageAbbreviation(mComponentName) + "]:"
+ + " isRCS = " + this.mIsRemoteConnectionService + ": " + msg, params);
}
private void queryRemoteConnectionServices(final UserHandle userHandle,
@@ -2508,6 +2514,7 @@
ConnectionServiceWrapper service = mConnectionServiceRepository.getService(
handle.getComponentName(), handle.getUserHandle());
if (service != null && service != this) {
+ service.mIsRemoteConnectionService = true;
simServices.add(service);
} else {
// This is unexpected, normally PhoneAccounts with CAPABILITY_CALL_PROVIDER are not
diff --git a/src/com/android/server/telecom/EmergencyCallHelper.java b/src/com/android/server/telecom/EmergencyCallHelper.java
index fbb666d..5ab0e99 100644
--- a/src/com/android/server/telecom/EmergencyCallHelper.java
+++ b/src/com/android/server/telecom/EmergencyCallHelper.java
@@ -82,10 +82,17 @@
return mLastEmergencyCallTimestampMillis;
}
- void setLastOutgoingEmergencyCallPAH(PhoneAccountHandle accountHandle) {
+ @VisibleForTesting
+ public void setLastOutgoingEmergencyCallTimestampMillis(long timestampMillis) {
+ mLastOutgoingEmergencyCallTimestampMillis = timestampMillis;
+ }
+
+ @VisibleForTesting
+ public void setLastOutgoingEmergencyCallPAH(PhoneAccountHandle accountHandle) {
mLastOutgoingEmergencyCallPAH = accountHandle;
}
+ @VisibleForTesting
public boolean isLastOutgoingEmergencyCallPAH(PhoneAccountHandle currentCallHandle) {
boolean ecbmActive = mLastOutgoingEmergencyCallPAH != null
&& isInEmergencyCallbackWindow(mLastOutgoingEmergencyCallTimestampMillis)
diff --git a/src/com/android/server/telecom/LogUtils.java b/src/com/android/server/telecom/LogUtils.java
index 4e7546f..0d6acd5 100644
--- a/src/com/android/server/telecom/LogUtils.java
+++ b/src/com/android/server/telecom/LogUtils.java
@@ -224,6 +224,7 @@
public static final String FLASH_NOTIFICATION_START = "FLASH_NOTIFICATION_START";
public static final String FLASH_NOTIFICATION_STOP = "FLASH_NOTIFICATION_STOP";
public static final String GAINED_FGS_DELEGATION = "GAINED_FGS_DELEGATION";
+ public static final String GAIN_FGS_DELEGATION_FAILED = "GAIN_FGS_DELEGATION_FAILED";
public static final String LOST_FGS_DELEGATION = "LOST_FGS_DELEGATION";
public static final String START_STREAMING = "START_STREAMING";
public static final String STOP_STREAMING = "STOP_STREAMING";
diff --git a/src/com/android/server/telecom/Ringer.java b/src/com/android/server/telecom/Ringer.java
index cdacab0..45fb2af 100644
--- a/src/com/android/server/telecom/Ringer.java
+++ b/src/com/android/server/telecom/Ringer.java
@@ -626,10 +626,9 @@
public boolean shouldRingForContact(Call call) {
// avoid re-computing manager.matcherCallFilter(Bundle)
if (call.wasDndCheckComputedForCall()) {
- Log.v(this, "shouldRingForContact: returning computation from DndCallFilter.");
+ Log.i(this, "shouldRingForContact: returning computation from DndCallFilter.");
return !call.isCallSuppressedByDoNotDisturb();
}
-
final Uri contactUri = call.getHandle();
final Bundle peopleExtras = new Bundle();
if (contactUri != null) {
@@ -637,13 +636,7 @@
personList.add(new Person.Builder().setUri(contactUri.toString()).build());
peopleExtras.putParcelableArrayList(Notification.EXTRA_PEOPLE_LIST, personList);
}
-
- // query NotificationManager
- boolean shouldRing = mNotificationManager.matchesCallFilter(peopleExtras);
- // store the suppressed status in the call object
- call.setCallIsSuppressedByDoNotDisturb(!shouldRing);
-
- return shouldRing;
+ return mNotificationManager.matchesCallFilter(peopleExtras);
}
private boolean hasExternalRinger(Call foregroundCall) {
diff --git a/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java b/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java
index 0be90e0..523b841 100644
--- a/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java
+++ b/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java
@@ -101,6 +101,10 @@
public static final String ACTION_CANCEL_REDIRECTED_CALL =
"com.android.server.telecom.CANCEL_REDIRECTED_CALL";
+ public static final String ACTION_HANGUP_CALL = "com.android.server.telecom.HANGUP_CALL";
+ public static final String ACTION_STOP_STREAMING =
+ "com.android.server.telecom.ACTION_STOP_STREAMING";
+
public static final String EXTRA_USERHANDLE = "userhandle";
public static final String EXTRA_REDIRECTION_OUTGOING_CALL_ID =
"android.telecom.extra.REDIRECTION_OUTGOING_CALL_ID";
@@ -242,6 +246,26 @@
} finally {
Log.endSession();
}
+ } else if (ACTION_HANGUP_CALL.equals(action)) {
+ Log.startSession("TBIP.aHC", "streamingDialog");
+ try {
+ Call call = mCallsManager.getCall(intent.getData().getSchemeSpecificPart());
+ if (call != null) {
+ mCallsManager.disconnectCall(call);
+ }
+ } finally {
+ Log.endSession();
+ }
+ } else if (ACTION_STOP_STREAMING.equals(action)) {
+ Log.startSession("TBIP.aSS", "streamingDialog");
+ try {
+ Call call = mCallsManager.getCall(intent.getData().getSchemeSpecificPart());
+ if (call != null) {
+ mCallsManager.stopCallStreaming(call);
+ }
+ } finally {
+ Log.endSession();
+ }
}
}
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 70f3491..3bfb933 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -48,6 +48,7 @@
import com.android.server.telecom.components.UserCallIntentProcessor;
import com.android.server.telecom.components.UserCallIntentProcessorFactory;
import com.android.server.telecom.ui.AudioProcessingNotification;
+import com.android.server.telecom.ui.CallStreamingNotification;
import com.android.server.telecom.ui.DisconnectedCallNotifier;
import com.android.server.telecom.ui.IncomingCallNotifier;
import com.android.server.telecom.ui.MissedCallNotifierImpl.MissedCallNotifierImplFactory;
@@ -354,6 +355,12 @@
mLock, timeoutsAdapter, clockProxy, emergencyCallDiagnosticLogger);
TransactionManager transactionManager = TransactionManager.getInstance();
+
+ CallStreamingNotification callStreamingNotification =
+ new CallStreamingNotification(mContext,
+ packageName -> AppLabelProxy.Util.getAppLabel(
+ mContext.getPackageManager(), packageName), asyncTaskExecutor);
+
mCallsManager = new CallsManager(
mContext,
mLock,
@@ -391,7 +398,9 @@
blockedNumbersAdapter,
transactionManager,
emergencyCallDiagnosticLogger,
- communicationDeviceTracker);
+ communicationDeviceTracker,
+ callStreamingNotification);
+
mIncomingCallNotifier = incomingCallNotifier;
incomingCallNotifier.setCallsManagerProxy(new IncomingCallNotifier.CallsManagerProxy() {
@Override
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
index d83e551..ec95f39 100644
--- a/src/com/android/server/telecom/TransactionalServiceWrapper.java
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -682,6 +682,7 @@
public void stopCallStreaming(Call call) {
+ Log.i(this, "stopCallStreaming; callid=%s", call.getId());
if (call != null && call.isStreaming()) {
VoipCallTransaction stopStreamingTransaction = createStopStreamingTransaction(call);
addTransactionsToManager(stopStreamingTransaction, new ResultReceiver(null));
diff --git a/src/com/android/server/telecom/ui/CallStreamingNotification.java b/src/com/android/server/telecom/ui/CallStreamingNotification.java
new file mode 100644
index 0000000..3f09bb1
--- /dev/null
+++ b/src/com/android/server/telecom/ui/CallStreamingNotification.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright (C) 2023 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.ui;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Person;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.telecom.Log;
+import android.telecom.PhoneAccount;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.ForegroundColorSpan;
+import androidx.core.graphics.drawable.RoundedBitmapDrawable;
+import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.telecom.AppLabelProxy;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManagerListenerBase;
+import com.android.server.telecom.R;
+import com.android.server.telecom.TelecomBroadcastIntentProcessor;
+import com.android.server.telecom.components.TelecomBroadcastReceiver;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Class responsible for tracking if there is a call which is being streamed and posting a
+ * notification which informs the user that a call is streaming. The user has two possible actions:
+ * disconnect the call, bring the call back to the current device (stop streaming).
+ */
+public class CallStreamingNotification extends CallsManagerListenerBase implements Call.Listener {
+ // URI scheme used for data related to the notification actions.
+ public static final String CALL_ID_SCHEME = "callid";
+ // The default streaming notification ID.
+ private static final int STREAMING_NOTIFICATION_ID = 90210;
+ // Tag for streaming notification.
+ private static final String NOTIFICATION_TAG =
+ CallStreamingNotification.class.getSimpleName();
+
+ private final Context mContext;
+ private final NotificationManager mNotificationManager;
+ // Used to get the app name for the notification.
+ private final AppLabelProxy mAppLabelProxy;
+ // An executor that can be used to fire off async tasks that do not block Telecom in any manner.
+ private final Executor mAsyncTaskExecutor;
+ // The call which is streaming.
+ private Call mStreamingCall;
+ // Lock for notification post/remove -- these happen outside the Telecom sync lock.
+ private final Object mNotificationLock = new Object();
+
+ // Whether the notification is showing.
+ @GuardedBy("mNotificationLock")
+ private boolean mIsNotificationShowing = false;
+ @GuardedBy("mNotificationLock")
+ private UserHandle mNotificationUserHandle;
+
+ public CallStreamingNotification(@NonNull Context context,
+ @NonNull AppLabelProxy appLabelProxy,
+ @NonNull Executor asyncTaskExecutor) {
+ mContext = context;
+ mNotificationManager = context.getSystemService(NotificationManager.class);
+ mAppLabelProxy = appLabelProxy;
+ mAsyncTaskExecutor = asyncTaskExecutor;
+ }
+
+ @Override
+ public void onCallAdded(Call call) {
+ if (call.isStreaming()) {
+ trackStreamingCall(call);
+ enqueueStreamingNotification(call);
+ }
+ }
+
+ @Override
+ public void onCallRemoved(Call call) {
+ if (call == mStreamingCall) {
+ trackStreamingCall(null);
+ dequeueStreamingNotification();
+ }
+ }
+
+ /**
+ * Handles streaming state changes for a call.
+ * @param call the call
+ * @param isStreaming whether it is streaming or not
+ */
+ @Override
+ public void onCallStreamingStateChanged(Call call, boolean isStreaming) {
+ Log.i(this, "onCallStreamingStateChanged: call=%s, isStreaming=%b", call.getId(),
+ isStreaming);
+
+ if (isStreaming) {
+ trackStreamingCall(call);
+ enqueueStreamingNotification(call);
+ } else {
+ trackStreamingCall(null);
+ dequeueStreamingNotification();
+ }
+ }
+
+ /**
+ * Change the streaming call we are tracking.
+ * @param call the call.
+ */
+ private void trackStreamingCall(Call call) {
+ if (mStreamingCall != null) {
+ mStreamingCall.removeListener(this);
+ }
+ mStreamingCall = call;
+ if (mStreamingCall != null) {
+ mStreamingCall.addListener(this);
+ }
+ }
+
+ /**
+ * Enqueue an async task to post/repost the streaming notification.
+ * Note: This happens INSIDE the telecom lock.
+ * @param call the call to post notification for.
+ */
+ private void enqueueStreamingNotification(Call call) {
+ final Bitmap contactPhotoBitmap = call.getPhotoIcon();
+ mAsyncTaskExecutor.execute(() -> {
+ Icon contactPhotoIcon = null;
+ try {
+ contactPhotoIcon = Icon.createWithResource(mContext.getResources(),
+ R.drawable.person_circle);
+ } catch (Exception e) {
+ // All loads of things can do wrong when working with bitmaps and images, so to
+ // ensure Telecom doesn't crash, lets try/catch to be sure.
+ Log.e(this, e, "enqueueStreamingNotification: Couldn't build avatar icon");
+ }
+ showStreamingNotification(call.getId(),
+ call.getUserHandleFromTargetPhoneAccount(), call.getCallerDisplayName(),
+ call.getHandle(), contactPhotoIcon,
+ call.getTargetPhoneAccount().getComponentName().getPackageName(),
+ call.getConnectTimeMillis());
+ });
+ }
+
+ /**
+ * Dequeues the call streaming notification.
+ * Note: This is yo be called within the Telecom sync lock to launch the task to remove the call
+ * streaming notification.
+ */
+ private void dequeueStreamingNotification() {
+ mAsyncTaskExecutor.execute(() -> hideStreamingNotification());
+ }
+
+ /**
+ * Show the call streaming notification. This is intended to run outside the Telecom sync lock.
+ *
+ * @param callId the call ID we're streaming.
+ * @param userHandle the userhandle for the call.
+ * @param callerName the name of the caller/callee associated with the call
+ * @param callerAddress the address associated with the caller/callee
+ * @param photoIcon the contact photo icon if available
+ * @param appPackageName the package name for the app to post the notification for
+ * @param connectTimeMillis when the call connected (for chronometer in the notification)
+ */
+ private void showStreamingNotification(final String callId, final UserHandle userHandle,
+ String callerName, Uri callerAddress, Icon photoIcon, String appPackageName,
+ long connectTimeMillis) {
+ Log.i(this, "showStreamingNotification; callid=%s, hasPhoto=%b", callId, photoIcon != null);
+
+ // 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();
+ }
+
+ // Action to hangup; this can use the default hangup action from the call style
+ // notification.
+ Intent hangupIntent = new Intent(TelecomBroadcastIntentProcessor.ACTION_HANGUP_CALL,
+ Uri.fromParts(CALL_ID_SCHEME, callId, null),
+ mContext, TelecomBroadcastReceiver.class);
+ PendingIntent hangupPendingIntent = PendingIntent.getBroadcast(mContext, 0, hangupIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+ // Action to switch here.
+ Intent switchHereIntent = new Intent(TelecomBroadcastIntentProcessor.ACTION_STOP_STREAMING,
+ Uri.fromParts(CALL_ID_SCHEME, callId, null),
+ mContext, TelecomBroadcastReceiver.class);
+ PendingIntent switchHerePendingIntent = PendingIntent.getBroadcast(mContext, 0,
+ switchHereIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+ // Apply a span to the string to colorize it using the "answer" color.
+ Spannable spannable = new SpannableString(
+ mContext.getString(R.string.call_streaming_notification_action_switch_here));
+ spannable.setSpan(new ForegroundColorSpan(
+ com.android.internal.R.color.call_notification_answer_color), 0, spannable.length(),
+ Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+
+ // Use the "phone link" icon per mock.
+ Icon switchHereIcon = Icon.createWithResource(mContext, R.drawable.gm_phonelink);
+ Notification.Action.Builder switchHereBuilder = new Notification.Action.Builder(
+ switchHereIcon,
+ spannable,
+ switchHerePendingIntent);
+ Notification.Action switchHereAction = switchHereBuilder.build();
+
+ // Notifications use a "person" entity to identify caller/callee.
+ Person.Builder personBuilder = new Person.Builder()
+ .setName(callerName);
+
+ // Some apps use phone numbers to identify; these are something the notification framework
+ // can lookup in contacts to provide more data
+ if (callerAddress != null && PhoneAccount.SCHEME_TEL.equals(callerAddress)) {
+ personBuilder.setUri(callerAddress.toString());
+ }
+ if (photoIcon != null) {
+ personBuilder.setIcon(photoIcon);
+ }
+ Person person = personBuilder.build();
+
+ // Call Style notification requires a full screen intent, so we'll just link in a null
+ // pending intent
+ Intent nullIntent = new Intent();
+ PendingIntent nullPendingIntent = PendingIntent.getBroadcast(mContext, 0, nullIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+ Notification.Builder builder = new Notification.Builder(mContext,
+ NotificationChannelManager.CHANNEL_ID_CALL_STREAMING)
+ // Use call style to get the general look and feel for the notification; it provides
+ // a hangup action with the right action already so we can leverage that. The
+ // "switch here" action will be a custom action defined later.
+ .setStyle(Notification.CallStyle.forOngoingCall(person, hangupPendingIntent))
+ .setSmallIcon(R.drawable.ic_phone)
+ .setContentText(mContext.getString(
+ R.string.call_streaming_notification_body))
+ // Report call time
+ .setWhen(connectTimeMillis)
+ .setShowWhen(true)
+ .setUsesChronometer(true)
+ // Set the full screen intent; this is just tricking notification manager into
+ // letting us use this style. Sssh.
+ .setFullScreenIntent(nullPendingIntent, true)
+ .setColorized(true)
+ .addAction(switchHereAction);
+ Notification notification = builder.build();
+
+ synchronized(mNotificationLock) {
+ mIsNotificationShowing = true;
+ mNotificationUserHandle = userHandle;
+ try {
+ mNotificationManager.notifyAsUser(NOTIFICATION_TAG, STREAMING_NOTIFICATION_ID,
+ notification, userHandle);
+ } catch (Exception e) {
+ // We don't want to crash Telecom if something changes with the requirements for the
+ // notification.
+ Log.e(this, e, "Notification post failed.");
+ }
+ }
+ }
+
+ /**
+ * Removes the posted streaming notification. Intended to run outside the telecom lock.
+ */
+ private void hideStreamingNotification() {
+ Log.i(this, "hideStreamingNotification");
+ synchronized(mNotificationLock) {
+ if (mIsNotificationShowing) {
+ mIsNotificationShowing = false;
+ mNotificationManager.cancelAsUser(NOTIFICATION_TAG,
+ STREAMING_NOTIFICATION_ID, mNotificationUserHandle);
+ }
+ }
+ }
+
+ public static Bitmap drawableToBitmap(@Nullable Drawable drawable, int width, int height) {
+ if (drawable == null) {
+ return null;
+ }
+
+ Bitmap bitmap;
+ if (drawable instanceof BitmapDrawable) {
+ bitmap = ((BitmapDrawable) drawable).getBitmap();
+ } else {
+ if (width > 0 || height > 0) {
+ bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ } else if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
+ // Needed for drawables that are just a colour.
+ bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+ } else {
+ bitmap =
+ Bitmap.createBitmap(
+ drawable.getIntrinsicWidth(),
+ drawable.getIntrinsicHeight(),
+ Bitmap.Config.ARGB_8888);
+ }
+
+ Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+ }
+ return bitmap;
+ }
+}
diff --git a/src/com/android/server/telecom/ui/NotificationChannelManager.java b/src/com/android/server/telecom/ui/NotificationChannelManager.java
index 58794a6..a0baa03 100644
--- a/src/com/android/server/telecom/ui/NotificationChannelManager.java
+++ b/src/com/android/server/telecom/ui/NotificationChannelManager.java
@@ -40,6 +40,7 @@
public static final String CHANNEL_ID_AUDIO_PROCESSING = "TelecomBackgroundAudioProcessing";
public static final String CHANNEL_ID_DISCONNECTED_CALLS = "TelecomDisconnectedCalls";
public static final String CHANNEL_ID_IN_CALL_SERVICE_CRASH = "TelecomInCallServiceCrash";
+ public static final String CHANNEL_ID_CALL_STREAMING = "TelecomCallStreaming";
private BroadcastReceiver mLocaleChangeReceiver = new BroadcastReceiver() {
@Override
@@ -63,6 +64,7 @@
createOrUpdateChannel(context, CHANNEL_ID_AUDIO_PROCESSING);
createOrUpdateChannel(context, CHANNEL_ID_DISCONNECTED_CALLS);
createOrUpdateChannel(context, CHANNEL_ID_IN_CALL_SERVICE_CRASH);
+ createOrUpdateChannel(context, CHANNEL_ID_CALL_STREAMING);
}
private void createOrUpdateChannel(Context context, String channelId) {
@@ -127,6 +129,14 @@
lights = true;
vibration = true;
sound = null;
+ case CHANNEL_ID_CALL_STREAMING:
+ name = context.getText(R.string.notification_channel_call_streaming);
+ importance = NotificationManager.IMPORTANCE_DEFAULT;
+ canShowBadge = false;
+ lights = false;
+ vibration = false;
+ sound = null;
+ break;
}
NotificationChannel channel = new NotificationChannel(channelId, name, importance);
diff --git a/src/com/android/server/telecom/voip/IncomingCallTransaction.java b/src/com/android/server/telecom/voip/IncomingCallTransaction.java
index c0bb93d..d35030c 100644
--- a/src/com/android/server/telecom/voip/IncomingCallTransaction.java
+++ b/src/com/android/server/telecom/voip/IncomingCallTransaction.java
@@ -17,6 +17,7 @@
package com.android.server.telecom.voip;
import static android.telecom.CallAttributes.CALL_CAPABILITIES_KEY;
+import static android.telecom.CallAttributes.DISPLAY_NAME_KEY;
import android.os.Bundle;
import android.telecom.CallAttributes;
@@ -80,6 +81,9 @@
mExtras.putString(TelecomManager.TRANSACTION_CALL_ID_KEY, mCallId);
mExtras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
mExtras.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE, callAttributes.getCallType());
+ mExtras.putCharSequence(DISPLAY_NAME_KEY, callAttributes.getDisplayName());
+ mExtras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
+ callAttributes.getAddress());
return mExtras;
}
}
diff --git a/src/com/android/server/telecom/voip/OutgoingCallTransaction.java b/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
index 0b17da2..b2625e6 100644
--- a/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
+++ b/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
@@ -18,6 +18,7 @@
import static android.Manifest.permission.CALL_PRIVILEGED;
import static android.telecom.CallAttributes.CALL_CAPABILITIES_KEY;
+import static android.telecom.CallAttributes.DISPLAY_NAME_KEY;
import static android.telecom.CallException.CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME;
import android.content.Context;
@@ -126,6 +127,7 @@
mExtras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
mExtras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
callAttributes.getCallType());
+ mExtras.putCharSequence(DISPLAY_NAME_KEY, callAttributes.getDisplayName());
return mExtras;
}
}
diff --git a/src/com/android/server/telecom/voip/ParallelTransaction.java b/src/com/android/server/telecom/voip/ParallelTransaction.java
index 2762949..6176087 100644
--- a/src/com/android/server/telecom/voip/ParallelTransaction.java
+++ b/src/com/android/server/telecom/voip/ParallelTransaction.java
@@ -16,9 +16,11 @@
package com.android.server.telecom.voip;
+import com.android.server.telecom.LoggedHandlerExecutor;
import com.android.server.telecom.TelecomSystem;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -33,15 +35,19 @@
@Override
public void start() {
// post timeout work
- mHandler.postDelayed(() -> {
+ CompletableFuture<Void> future = new CompletableFuture<>();
+ mHandler.postDelayed(() -> future.complete(null), TIMEOUT_LIMIT);
+ future.thenApplyAsync((x) -> {
if (mCompleted.getAndSet(true)) {
- return;
+ return null;
}
if (mCompleteListener != null) {
mCompleteListener.onTransactionTimeout(mTransactionName);
}
finish();
- }, TIMEOUT_LIMIT);
+ return null;
+ }, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
+ + ".s", mLock));
if (mSubTransactions != null && mSubTransactions.size() > 0) {
TransactionManager.TransactionCompleteListener subTransactionListener =
@@ -52,16 +58,21 @@
public void onTransactionCompleted(VoipCallTransactionResult result,
String transactionName) {
if (result.getResult() != VoipCallTransactionResult.RESULT_SUCCEED) {
- mHandler.post(() -> {
- VoipCallTransactionResult mainResult =
- new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
- String.format("sub transaction %s failed",
- transactionName));
- mCompleteListener.onTransactionCompleted(mainResult,
- mTransactionName);
- finish();
- });
+ CompletableFuture.completedFuture(null).thenApplyAsync(
+ (x) -> {
+ VoipCallTransactionResult mainResult =
+ new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_FAILED,
+ String.format(
+ "sub transaction %s failed",
+ transactionName));
+ mCompleteListener.onTransactionCompleted(mainResult,
+ mTransactionName);
+ finish();
+ return null;
+ }, new LoggedHandlerExecutor(mHandler,
+ mTransactionName + "@" + hashCode()
+ + ".oTC", mLock));
} else {
if (mCount.decrementAndGet() == 0) {
scheduleTransaction();
@@ -71,15 +82,20 @@
@Override
public void onTransactionTimeout(String transactionName) {
- mHandler.post(() -> {
- VoipCallTransactionResult mainResult = new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
- String.format("sub transaction %s timed out",
- transactionName));
- mCompleteListener.onTransactionCompleted(mainResult,
- mTransactionName);
- finish();
- });
+ CompletableFuture.completedFuture(null).thenApplyAsync(
+ (x) -> {
+ VoipCallTransactionResult mainResult =
+ new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_FAILED,
+ String.format("sub transaction %s timed out",
+ transactionName));
+ mCompleteListener.onTransactionCompleted(mainResult,
+ mTransactionName);
+ finish();
+ return null;
+ }, new LoggedHandlerExecutor(mHandler,
+ mTransactionName + "@" + hashCode()
+ + ".oTT", mLock));
}
};
for (VoipCallTransaction transaction : mSubTransactions) {
diff --git a/src/com/android/server/telecom/voip/SerialTransaction.java b/src/com/android/server/telecom/voip/SerialTransaction.java
index 4c6d02e..5d5d1f3 100644
--- a/src/com/android/server/telecom/voip/SerialTransaction.java
+++ b/src/com/android/server/telecom/voip/SerialTransaction.java
@@ -16,9 +16,11 @@
package com.android.server.telecom.voip;
+import com.android.server.telecom.LoggedHandlerExecutor;
import com.android.server.telecom.TelecomSystem;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
/**
* A VoipCallTransaction implementation that its sub transactions will be executed in serial
@@ -36,15 +38,19 @@
@Override
public void start() {
// post timeout work
- mHandler.postDelayed(() -> {
+ CompletableFuture<Void> future = new CompletableFuture<>();
+ mHandler.postDelayed(() -> future.complete(null), TIMEOUT_LIMIT);
+ future.thenApplyAsync((x) -> {
if (mCompleted.getAndSet(true)) {
- return;
+ return null;
}
if (mCompleteListener != null) {
mCompleteListener.onTransactionTimeout(mTransactionName);
}
finish();
- }, TIMEOUT_LIMIT);
+ return null;
+ }, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
+ + ".s", mLock));
if (mSubTransactions != null && mSubTransactions.size() > 0) {
TransactionManager.TransactionCompleteListener subTransactionListener =
@@ -54,16 +60,21 @@
public void onTransactionCompleted(VoipCallTransactionResult result,
String transactionName) {
if (result.getResult() != VoipCallTransactionResult.RESULT_SUCCEED) {
- mHandler.post(() -> {
- VoipCallTransactionResult mainResult =
- new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
- String.format("sub transaction %s failed",
- transactionName));
- mCompleteListener.onTransactionCompleted(mainResult,
- mTransactionName);
- finish();
- });
+ CompletableFuture.completedFuture(null).thenApplyAsync(
+ (x) -> {
+ VoipCallTransactionResult mainResult =
+ new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_FAILED,
+ String.format(
+ "sub transaction %s failed",
+ transactionName));
+ mCompleteListener.onTransactionCompleted(mainResult,
+ mTransactionName);
+ finish();
+ return null;
+ }, new LoggedHandlerExecutor(mHandler,
+ mTransactionName + "@" + hashCode()
+ + ".oTC", mLock));
} else {
if (mSubTransactions.size() > 0) {
VoipCallTransaction transaction = mSubTransactions.remove(0);
@@ -77,15 +88,20 @@
@Override
public void onTransactionTimeout(String transactionName) {
- mHandler.post(() -> {
- VoipCallTransactionResult mainResult = new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
- String.format("sub transaction %s timed out",
- transactionName));
- mCompleteListener.onTransactionCompleted(mainResult,
- mTransactionName);
- finish();
- });
+ CompletableFuture.completedFuture(null).thenApplyAsync(
+ (x) -> {
+ VoipCallTransactionResult mainResult =
+ new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_FAILED,
+ String.format("sub transaction %s timed out",
+ transactionName));
+ mCompleteListener.onTransactionCompleted(mainResult,
+ mTransactionName);
+ finish();
+ return null;
+ }, new LoggedHandlerExecutor(mHandler,
+ mTransactionName + "@" + hashCode()
+ + ".oTT", mLock));
}
};
VoipCallTransaction transaction = mSubTransactions.remove(0);
diff --git a/src/com/android/server/telecom/voip/TransactionManager.java b/src/com/android/server/telecom/voip/TransactionManager.java
index 773dfb8..228bdde 100644
--- a/src/com/android/server/telecom/voip/TransactionManager.java
+++ b/src/com/android/server/telecom/voip/TransactionManager.java
@@ -70,8 +70,8 @@
@Override
public void onTransactionCompleted(VoipCallTransactionResult result,
String transactionName){
- Log.i(TAG, String.format("transaction completed: with result=[%d]",
- result.getResult()));
+ Log.i(TAG, String.format("transaction %s completed: with result=[%d]",
+ transactionName, result.getResult()));
if (result.getResult() == TelecomManager.TELECOM_TRANSACTION_SUCCESS) {
receiver.onResult(result);
} else {
@@ -84,6 +84,7 @@
@Override
public void onTransactionTimeout(String transactionName){
+ Log.i(TAG, String.format("transaction %s timeout", transactionName));
receiver.onError(new CallException(transactionName + " timeout",
CODE_OPERATION_TIMED_OUT));
finishTransaction();
diff --git a/src/com/android/server/telecom/voip/VoipCallMonitor.java b/src/com/android/server/telecom/voip/VoipCallMonitor.java
index 2a81051..9254395 100644
--- a/src/com/android/server/telecom/voip/VoipCallMonitor.java
+++ b/src/com/android/server/telecom/voip/VoipCallMonitor.java
@@ -38,6 +38,7 @@
import com.android.server.telecom.Call;
import com.android.server.telecom.CallsManagerListenerBase;
import com.android.server.telecom.LogUtils;
+import com.android.server.telecom.LoggedHandlerExecutor;
import com.android.server.telecom.TelecomSystem;
import java.util.ArrayList;
@@ -46,14 +47,15 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.CompletableFuture;
public class VoipCallMonitor extends CallsManagerListenerBase {
- private final List<Call> mPendingCalls;
+ private final List<Call> mNotificationPendingCalls;
// Same notification may be passed as different object in onNotificationPosted and
// onNotificationRemoved. Use its string as key to cache ongoing notifications.
- private final Map<String, Call> mNotifications;
- private final Map<PhoneAccountHandle, Set<Call>> mPhoneAccountHandleListMap;
+ private final Map<NotificationInfo, Call> mNotificationInfoToCallMap;
+ private final Map<PhoneAccountHandle, Set<Call>> mAccountHandleToCallMap;
private ActivityManagerInternal mActivityManagerInternal;
private final Map<PhoneAccountHandle, ServiceConnection> mServices;
private NotificationListenerService mNotificationListener;
@@ -61,18 +63,20 @@
private final HandlerThread mHandlerThread;
private final Handler mHandler;
private final Context mContext;
- private List<StatusBarNotification> mPendingSBN;
+ private List<NotificationInfo> mCachedNotifications;
+ private TelecomSystem.SyncRoot mSyncRoot;
public VoipCallMonitor(Context context, TelecomSystem.SyncRoot lock) {
+ mSyncRoot = lock;
mContext = context;
mHandlerThread = new HandlerThread(this.getClass().getSimpleName());
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
- mPendingCalls = new ArrayList<>();
- mPendingSBN = new ArrayList<>();
- mNotifications = new HashMap<>();
+ mNotificationPendingCalls = new ArrayList<>();
+ mCachedNotifications = new ArrayList<>();
+ mNotificationInfoToCallMap = new HashMap<>();
mServices = new HashMap<>();
- mPhoneAccountHandleListMap = new HashMap<>();
+ mAccountHandleToCallMap = new HashMap<>();
mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
mNotificationListener = new NotificationListenerService() {
@@ -80,19 +84,21 @@
public void onNotificationPosted(StatusBarNotification sbn) {
synchronized (mLock) {
if (sbn.getNotification().isStyle(Notification.CallStyle.class)) {
+ NotificationInfo info = new NotificationInfo(sbn.getPackageName(),
+ sbn.getUser());
boolean sbnMatched = false;
- for (Call call : mPendingCalls) {
- if (notificationMatchedCall(sbn, call)) {
- mPendingCalls.remove(call);
- mNotifications.put(sbn.toString(), call);
+ for (Call call : mNotificationPendingCalls) {
+ if (info.matchesCall(call)) {
+ mNotificationPendingCalls.remove(call);
+ mNotificationInfoToCallMap.put(info, call);
sbnMatched = true;
break;
}
}
if (!sbnMatched) {
- // notification may posted before we started to monitor the call, cache
+ // notification may post before we started to monitor the call, cache
// this notification and try to match it later with new added call.
- mPendingSBN.add(sbn);
+ mCachedNotifications.add(info);
}
}
}
@@ -101,14 +107,17 @@
@Override
public void onNotificationRemoved(StatusBarNotification sbn) {
synchronized (mLock) {
- mPendingSBN.remove(sbn);
- if (mNotifications.isEmpty()) {
+ NotificationInfo info = new NotificationInfo(sbn.getPackageName(),
+ sbn.getUser());
+ mCachedNotifications.remove(info);
+ if (mNotificationInfoToCallMap.isEmpty()) {
return;
}
- Call call = mNotifications.getOrDefault(sbn.toString(), null);
+ Call call = mNotificationInfoToCallMap.getOrDefault(info, null);
if (call != null) {
- mNotifications.remove(sbn.toString(), call);
- stopFGSDelegation(call.getTargetPhoneAccount());
+ // TODO: fix potential bug for multiple calls of same voip app.
+ mNotificationInfoToCallMap.remove(info, call);
+ stopFGSDelegation(call);
}
}
}
@@ -142,13 +151,16 @@
synchronized (mLock) {
PhoneAccountHandle phoneAccountHandle = call.getTargetPhoneAccount();
- Set<Call> callList = mPhoneAccountHandleListMap.computeIfAbsent(phoneAccountHandle,
+ Set<Call> callList = mAccountHandleToCallMap.computeIfAbsent(phoneAccountHandle,
k -> new HashSet<>());
callList.add(call);
- mHandler.post(
- () -> startFGSDelegation(call.getCallingPackageIdentity().mCallingPackagePid,
- call.getCallingPackageIdentity().mCallingPackageUid, call));
+ CompletableFuture.completedFuture(null).thenComposeAsync(
+ (x) -> {
+ startFGSDelegation(call.getCallingPackageIdentity().mCallingPackagePid,
+ call.getCallingPackageIdentity().mCallingPackageUid, call);
+ return null;
+ }, new LoggedHandlerExecutor(mHandler, "VCM.oCA", mSyncRoot));
}
}
@@ -161,12 +173,12 @@
synchronized (mLock) {
stopMonitorWorks(call);
PhoneAccountHandle phoneAccountHandle = call.getTargetPhoneAccount();
- Set<Call> callList = mPhoneAccountHandleListMap.computeIfAbsent(phoneAccountHandle,
+ Set<Call> callList = mAccountHandleToCallMap.computeIfAbsent(phoneAccountHandle,
k -> new HashSet<>());
callList.remove(call);
if (callList.isEmpty()) {
- stopFGSDelegation(phoneAccountHandle);
+ stopFGSDelegation(call);
}
}
}
@@ -183,19 +195,22 @@
ServiceConnection fgsConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
- Log.addEvent(call, LogUtils.Events.GAINED_FGS_DELEGATION);
mServices.put(handle, this);
startMonitorWorks(call);
}
@Override
public void onServiceDisconnected(ComponentName name) {
- Log.addEvent(call, LogUtils.Events.LOST_FGS_DELEGATION);
mServices.remove(handle);
}
};
try {
- mActivityManagerInternal.startForegroundServiceDelegate(options, fgsConnection);
+ if (mActivityManagerInternal
+ .startForegroundServiceDelegate(options, fgsConnection)) {
+ Log.addEvent(call, LogUtils.Events.GAINED_FGS_DELEGATION);
+ } else {
+ Log.addEvent(call, LogUtils.Events.GAIN_FGS_DELEGATION_FAILED);
+ }
} catch (Exception e) {
Log.i(this, "startForegroundServiceDelegate failed due to: " + e);
}
@@ -203,21 +218,23 @@
}
@VisibleForTesting
- public void stopFGSDelegation(PhoneAccountHandle handle) {
+ public void stopFGSDelegation(Call call) {
synchronized (mLock) {
- Log.i(this, "stopFGSDelegation of handle %s", handle);
- Set<Call> calls = mPhoneAccountHandleListMap.get(handle);
+ Log.i(this, "stopFGSDelegation of call %s", call);
+ PhoneAccountHandle handle = call.getTargetPhoneAccount();
+ Set<Call> calls = mAccountHandleToCallMap.get(handle);
if (calls != null) {
- for (Call call : calls) {
- stopMonitorWorks(call);
+ for (Call c : calls) {
+ stopMonitorWorks(c);
}
}
- mPhoneAccountHandleListMap.remove(handle);
+ mAccountHandleToCallMap.remove(handle);
if (mActivityManagerInternal != null) {
ServiceConnection fgsConnection = mServices.get(handle);
if (fgsConnection != null) {
mActivityManagerInternal.stopForegroundServiceDelegate(fgsConnection);
+ Log.addEvent(call, LogUtils.Events.LOST_FGS_DELEGATION);
}
}
}
@@ -234,33 +251,36 @@
private void startMonitorNotification(Call call) {
synchronized (mLock) {
boolean sbnMatched = false;
- for (StatusBarNotification sbn : mPendingSBN) {
- if (notificationMatchedCall(sbn, call)) {
- mPendingSBN.remove(sbn);
- mNotifications.put(sbn.toString(), call);
+ for (NotificationInfo info : mCachedNotifications) {
+ if (info.matchesCall(call)) {
+ mCachedNotifications.remove(info);
+ mNotificationInfoToCallMap.put(info, call);
sbnMatched = true;
break;
}
}
if (!sbnMatched) {
// Only continue to
- mPendingCalls.add(call);
- mHandler.postDelayed(() -> {
- synchronized (mLock) {
- if (mPendingCalls.contains(call)) {
- Log.i(this, "Notification for voip-call %s haven't "
- + "posted in time, stop delegation.", call.getId());
- stopFGSDelegation(call.getTargetPhoneAccount());
- mPendingCalls.remove(call);
- }
- }
- }, 5000L);
+ mNotificationPendingCalls.add(call);
+ CompletableFuture<Void> future = new CompletableFuture<>();
+ mHandler.postDelayed(() -> future.complete(null), 5000L);
+ future.thenComposeAsync(
+ (x) -> {
+ if (mNotificationPendingCalls.contains(call)) {
+ Log.i(this, "Notification for voip-call %s haven't "
+ + "posted in time, stop delegation.", call.getId());
+ stopFGSDelegation(call);
+ mNotificationPendingCalls.remove(call);
+ return null;
+ }
+ return null;
+ }, new LoggedHandlerExecutor(mHandler, "VCM.sMN", mSyncRoot));
}
}
}
private void stopMonitorNotification(Call call) {
- mPendingCalls.remove(call);
+ mNotificationPendingCalls.remove(call);
}
@VisibleForTesting
@@ -273,15 +293,20 @@
mNotificationListener = listener;
}
- private boolean notificationMatchedCall(StatusBarNotification sbn, Call call) {
- String packageName = sbn.getPackageName();
- UserHandle userHandle = sbn.getUser();
- PhoneAccountHandle accountHandle = call.getTargetPhoneAccount();
+ private class NotificationInfo {
+ private String mPackageName;
+ private UserHandle mUserHandle;
- return packageName != null &&
- packageName.equals(call.getTargetPhoneAccount()
- .getComponentName().getPackageName())
- && userHandle != null
- && userHandle.equals(accountHandle.getUserHandle());
+ NotificationInfo(String packageName, UserHandle userHandle) {
+ mPackageName = packageName;
+ mUserHandle = userHandle;
+ }
+
+ boolean matchesCall(Call call) {
+ PhoneAccountHandle accountHandle = call.getTargetPhoneAccount();
+ return mPackageName != null && mPackageName.equals(
+ accountHandle.getComponentName().getPackageName())
+ && mUserHandle != null && mUserHandle.equals(accountHandle.getUserHandle());
+ }
}
}
diff --git a/src/com/android/server/telecom/voip/VoipCallTransaction.java b/src/com/android/server/telecom/voip/VoipCallTransaction.java
index a1cc13c..a952eb1 100644
--- a/src/com/android/server/telecom/voip/VoipCallTransaction.java
+++ b/src/com/android/server/telecom/voip/VoipCallTransaction.java
@@ -18,6 +18,7 @@
import android.os.Handler;
import android.os.HandlerThread;
+import android.telecom.Log;
import com.android.server.telecom.LoggedHandlerExecutor;
import com.android.server.telecom.TelecomSystem;
@@ -37,7 +38,7 @@
protected Handler mHandler;
protected TransactionManager.TransactionCompleteListener mCompleteListener;
protected List<VoipCallTransaction> mSubTransactions;
- private TelecomSystem.SyncRoot mLock;
+ protected TelecomSystem.SyncRoot mLock;
public VoipCallTransaction(
List<VoipCallTransaction> subTransactions, TelecomSystem.SyncRoot lock) {
@@ -54,33 +55,40 @@
public void start() {
// post timeout work
- mHandler.postDelayed(() -> {
+ CompletableFuture<Void> future = new CompletableFuture<>();
+ mHandler.postDelayed(() -> future.complete(null), TIMEOUT_LIMIT);
+ future.thenApplyAsync((x) -> {
if (mCompleted.getAndSet(true)) {
- return;
+ return null;
}
if (mCompleteListener != null) {
mCompleteListener.onTransactionTimeout(mTransactionName);
}
finish();
- }, TIMEOUT_LIMIT);
+ return null;
+ }, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
+ + ".s", mLock));
scheduleTransaction();
}
protected void scheduleTransaction() {
+ LoggedHandlerExecutor executor = new LoggedHandlerExecutor(mHandler,
+ mTransactionName + "@" + hashCode() + ".pT", mLock);
CompletableFuture<Void> future = CompletableFuture.completedFuture(null);
- future.thenComposeAsync(this::processTransaction,
- new LoggedHandlerExecutor(mHandler, mTransactionName + "@"
- + hashCode() + ".pT", mLock))
- .thenApplyAsync(
- (Function<VoipCallTransactionResult, Void>) result -> {
- mCompleted.set(true);
- if (mCompleteListener != null) {
- mCompleteListener.onTransactionCompleted(result, mTransactionName);
- }
- finish();
- return null;
- });
+ future.thenComposeAsync(this::processTransaction, executor)
+ .thenApplyAsync((Function<VoipCallTransactionResult, Void>) result -> {
+ mCompleted.set(true);
+ if (mCompleteListener != null) {
+ mCompleteListener.onTransactionCompleted(result, mTransactionName);
+ }
+ finish();
+ return null;
+ }, executor)
+ .exceptionallyAsync((throwable -> {
+ Log.e(this, throwable, "Error while executing transaction.");
+ return null;
+ }), executor);
}
public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
diff --git a/testapps/streamingtest/Android.bp b/testapps/streamingtest/Android.bp
new file mode 100644
index 0000000..bd0a582
--- /dev/null
+++ b/testapps/streamingtest/Android.bp
@@ -0,0 +1,31 @@
+//
+// Copyright (C) 2022 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "streamingTestApp",
+ static_libs: [
+ "androidx.legacy_legacy-support-v4",
+ "guava",
+ ],
+ srcs: ["src/**/*.java"],
+ platform_apis: true,
+ certificate: "platform",
+ privileged: true,
+}
diff --git a/testapps/streamingtest/AndroidManifest.xml b/testapps/streamingtest/AndroidManifest.xml
new file mode 100644
index 0000000..47e4abc
--- /dev/null
+++ b/testapps/streamingtest/AndroidManifest.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ coreApp="true"
+ package="com.android.server.telecom.streamingtest">
+
+ <uses-sdk android:minSdkVersion="28"
+ android:targetSdkVersion="33"/>
+
+ <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+ <uses-permission android:name="android.permission.CALL_AUDIO_INTERCEPTION"/>
+
+ <application android:label="Streaming Test App">
+ <uses-library android:name="android.test.runner"/>
+
+ <service android:name="com.android.server.telecom.streamingtest.StreamingService"
+ android:exported="true"
+ android:permission="android.permission.BIND_CALL_STREAMING_SERVICE">
+ <intent-filter>
+ <action android:name="android.telecom.CallStreamingService"/>
+ </intent-filter>
+ </service>
+ </application>
+</manifest>
diff --git a/testapps/streamingtest/src/com/android/server/telecom/streamingtest/StreamingService.java b/testapps/streamingtest/src/com/android/server/telecom/streamingtest/StreamingService.java
new file mode 100644
index 0000000..c76b349
--- /dev/null
+++ b/testapps/streamingtest/src/com/android/server/telecom/streamingtest/StreamingService.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 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.streamingtest;
+
+import android.annotation.NonNull;
+import android.content.Intent;
+import android.telecom.CallStreamingService;
+import android.telecom.StreamingCall;
+import android.telecom.Log;
+
+public class StreamingService extends CallStreamingService {
+ @Override
+ public void onCallStreamingStarted(@NonNull StreamingCall call) {
+ Log.i(this, "onCallStreamingStarted: call %s", call);
+ }
+
+ @Override
+ public void onCallStreamingStopped() {
+ Log.i(this, "onCallStreamingStopped");
+ }
+
+ @Override
+ public void onCallStreamingStateChanged(int state) {
+ Log.i(this, "onCallStreamingStateChanged; state=%d", state);
+ }
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ Log.i(this, "onUnbind");
+ return false;
+ }
+}
diff --git a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/InCallActivity.java b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/InCallActivity.java
index b868b70..3e53800 100644
--- a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/InCallActivity.java
+++ b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/InCallActivity.java
@@ -57,6 +57,11 @@
setContentView(R.layout.in_call_activity);
Bundle extras = getIntent().getExtras();
+ // Copy the extras with properties like call direction into the extras so the below
+ // code can access them.
+ if (extras != null && extras.containsKey(Utils.sEXTRAS_KEY)) {
+ extras.putAll(extras.getBundle(Utils.sEXTRAS_KEY));
+ }
if (extras != null) {
mCallDirection = extras.getInt(Utils.sCALL_DIRECTION_KEY, DIRECTION_INCOMING);
}
@@ -211,7 +216,7 @@
Utils.PHONE_ACCOUNT_HANDLE,
mCallDirection,
"Alan Turing",
- Uri.parse("tel:6506959001")).build();
+ Uri.parse("tel:+16506959001")).build();
mTelecomManager.addCall(callAttributes, Runnable::run,
new OutcomeReceiver<CallControl, CallException>() {
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
index d0a1d8b..d7854a5 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
@@ -117,7 +117,7 @@
when(mCallAudioManager.startRinging()).thenReturn(false);
sm.sendMessage(CallAudioModeStateMachine.START_CALL_STREAMING, new Builder()
- .setHasActiveOrDialingCalls(false)
+ .setHasActiveOrDialingCalls(true)
.setHasRingingCalls(false)
.setHasHoldingCalls(false)
.setIsTonePlaying(false)
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index afe7e18..ddb48ec 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -126,6 +126,7 @@
import com.android.server.telecom.callfiltering.BlockedNumbersAdapter;
import com.android.server.telecom.callfiltering.CallFilteringResult;
import com.android.server.telecom.ui.AudioProcessingNotification;
+import com.android.server.telecom.ui.CallStreamingNotification;
import com.android.server.telecom.ui.DisconnectedCallNotifier;
import com.android.server.telecom.ui.ToastFactory;
import com.android.server.telecom.voip.TransactionManager;
@@ -259,6 +260,7 @@
@Mock private BlockedNumbersAdapter mBlockedNumbersAdapter;
@Mock private PhoneCapability mPhoneCapability;
@Mock private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
+ @Mock private CallStreamingNotification mCallStreamingNotification;
private CallsManager mCallsManager;
@@ -330,7 +332,8 @@
mBlockedNumbersAdapter,
TransactionManager.getTestInstance(),
mEmergencyCallDiagnosticLogger,
- mCommunicationDeviceTracker);
+ mCommunicationDeviceTracker,
+ mCallStreamingNotification);
when(mPhoneAccountRegistrar.getPhoneAccount(
eq(SELF_MANAGED_HANDLE), any())).thenReturn(SELF_MANAGED_ACCOUNT);
@@ -942,6 +945,64 @@
@SmallTest
@Test
+ public void testAnswerThirdCallWhenTwoCallsOnDifferentSims_disconnectsHeldCall() {
+ // Given an ongoing call on SIM1
+ Call ongoingCall = addSpyCall(SIM_1_HANDLE, CallState.ACTIVE);
+ doReturn(true).when(ongoingCall).can(Connection.CAPABILITY_SUPPORT_HOLD);
+ doReturn(CallState.ACTIVE).when(ongoingCall).getState();
+ when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(ongoingCall);
+
+ // And a held call on SIM2, which belongs to the same ConnectionService
+ Call heldCall = addSpyCall(SIM_2_HANDLE, CallState.ON_HOLD);
+ doReturn(CallState.ON_HOLD).when(heldCall).getState();
+
+ // on answering an incoming call on SIM1, which belongs to the same ConnectionService
+ Call incomingCall = addSpyCall(SIM_1_HANDLE, CallState.RINGING);
+ mCallsManager.answerCall(incomingCall, VideoProfile.STATE_AUDIO_ONLY);
+
+ // THEN the previous held call is disconnected
+ verify(heldCall).disconnect();
+
+ // and the ongoing call is held
+ verify(ongoingCall).hold();
+
+ // and the focus request is sent
+ verifyFocusRequestAndExecuteCallback(incomingCall);
+ // and the incoming call is answered
+ verify(incomingCall).answer(VideoProfile.STATE_AUDIO_ONLY);
+ }
+
+ @SmallTest
+ @Test
+ public void testAnswerThirdCallDifferentSimWhenTwoCallsOnSameSim_disconnectsHeldCall() {
+ // Given an ongoing call on SIM1
+ Call ongoingCall = addSpyCall(SIM_1_HANDLE, CallState.ACTIVE);
+ doReturn(true).when(ongoingCall).can(Connection.CAPABILITY_SUPPORT_HOLD);
+ doReturn(CallState.ACTIVE).when(ongoingCall).getState();
+ when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(ongoingCall);
+
+ // And a held call on SIM1
+ Call heldCall = addSpyCall(SIM_1_HANDLE, CallState.ON_HOLD);
+ doReturn(CallState.ON_HOLD).when(heldCall).getState();
+
+ // on answering an incoming call on SIM2, which belongs to the same ConnectionService
+ Call incomingCall = addSpyCall(SIM_2_HANDLE, CallState.RINGING);
+ mCallsManager.answerCall(incomingCall, VideoProfile.STATE_AUDIO_ONLY);
+
+ // THEN the previous held call is disconnected
+ verify(heldCall).disconnect();
+
+ // and the ongoing call is held
+ verify(ongoingCall).hold();
+
+ // and the focus request is sent
+ verifyFocusRequestAndExecuteCallback(incomingCall);
+ // and the incoming call is answered
+ verify(incomingCall).answer(VideoProfile.STATE_AUDIO_ONLY);
+ }
+
+ @SmallTest
+ @Test
public void testAnswerCallWhenNoOngoingCallExisted() {
// GIVEN a CallsManager with no ongoing call.
diff --git a/tests/src/com/android/server/telecom/tests/EmergencyCallHelperTest.java b/tests/src/com/android/server/telecom/tests/EmergencyCallHelperTest.java
index 380e327..692d720 100644
--- a/tests/src/com/android/server/telecom/tests/EmergencyCallHelperTest.java
+++ b/tests/src/com/android/server/telecom/tests/EmergencyCallHelperTest.java
@@ -19,9 +19,11 @@
import static android.Manifest.permission.ACCESS_BACKGROUND_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
-import android.content.Context;
+import android.content.ComponentName;
+import android.content.ContentResolver;
import android.content.pm.PackageManager;
import android.os.UserHandle;
+import android.telecom.PhoneAccountHandle;
import android.test.suitebuilder.annotation.SmallTest;
import com.android.server.telecom.Call;
@@ -34,19 +36,20 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
-import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.any;
@RunWith(JUnit4.class)
public class EmergencyCallHelperTest extends TelecomTestCase {
@@ -62,6 +65,7 @@
private UserHandle mUserHandle;
@Mock
private Call mCall;
+ @Mock private PhoneAccountHandle mPhoneAccountHandle;
@Override
@Before
@@ -86,6 +90,8 @@
when(mCall.isEmergencyCall()).thenReturn(true);
when(mContext.getResources().getBoolean(R.bool.grant_location_permission_enabled)).thenReturn(
true);
+ when(mTimeoutsAdapter.getEmergencyCallbackWindowMillis(any(ContentResolver.class))).thenReturn(
+ 5000L);
}
@Override
@@ -245,4 +251,21 @@
verifyRevokeNotInvokedFor(ACCESS_FINE_LOCATION);
verifyRevokeInvokedFor(ACCESS_BACKGROUND_LOCATION);
}
+
+ @SmallTest
+ @Test
+ public void testIsLastOutgoingEmergencyCallPAH() {
+ PhoneAccountHandle dummyHandle = new PhoneAccountHandle(new ComponentName("pkg", "cls"), "foo");
+ long currentTimeMillis = System.currentTimeMillis();
+ mEmergencyCallHelper.setLastOutgoingEmergencyCallPAH(mPhoneAccountHandle);
+ mEmergencyCallHelper.setLastOutgoingEmergencyCallTimestampMillis(currentTimeMillis);
+
+ // Verify that ECBM is active on mPhoneAccountHandle.
+ assertTrue(mEmergencyCallHelper.isLastOutgoingEmergencyCallPAH(mPhoneAccountHandle));
+ assertFalse(mEmergencyCallHelper.isLastOutgoingEmergencyCallPAH(dummyHandle));
+
+ // Expire ECBM and verify that mPhoneAccountHandle is no longer supported for ECBM.
+ mEmergencyCallHelper.setLastOutgoingEmergencyCallTimestampMillis(currentTimeMillis/2);
+ assertFalse(mEmergencyCallHelper.isLastOutgoingEmergencyCallPAH(mPhoneAccountHandle));
+ }
}
diff --git a/tests/src/com/android/server/telecom/tests/RingerTest.java b/tests/src/com/android/server/telecom/tests/RingerTest.java
index abbfe34..a02415c 100644
--- a/tests/src/com/android/server/telecom/tests/RingerTest.java
+++ b/tests/src/com/android/server/telecom/tests/RingerTest.java
@@ -435,42 +435,65 @@
}
/**
- * assert {@link Ringer#shouldRingForContact(Call, Context) } sets the Call object with suppress
- * caller
- *
- * @throws Exception; should not throw exception.
+ * test shouldRingForContact will suppress the incoming call if matchesCallFilter returns
+ * false (meaning DND is ON and the caller cannot bypass the settings)
*/
@Test
- public void testShouldRingForContact_CallSuppressed() throws Exception {
+ public void testShouldRingForContact_CallSuppressed() {
// WHEN
when(mockCall1.wasDndCheckComputedForCall()).thenReturn(false);
when(mockCall1.getHandle()).thenReturn(Uri.parse(""));
-
when(mContext.getSystemService(NotificationManager.class)).thenReturn(
mockNotificationManager);
+ // suppress the call
when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(false);
- // THEN
+ // run the method under test
assertFalse(mRingerUnderTest.shouldRingForContact(mockCall1));
- verify(mockCall1, atLeastOnce()).setCallIsSuppressedByDoNotDisturb(true);
+
+ // THEN
+ // verify we never set the call object and matchesCallFilter is called
+ verify(mockCall1, never()).setCallIsSuppressedByDoNotDisturb(true);
+ verify(mockNotificationManager, times(1))
+ .matchesCallFilter(any(Bundle.class));
}
/**
- * assert {@link Ringer#shouldRingForContact(Call, Context) } sets the Call object with ring
- * caller
- *
- * @throws Exception; should not throw exception.
+ * test shouldRingForContact will alert the user of an incoming call if matchesCallFilter
+ * returns true (meaning DND is NOT suppressing the caller)
*/
@Test
- public void testShouldRingForContact_CallShouldRing() throws Exception {
+ public void testShouldRingForContact_CallShouldRing() {
// WHEN
when(mockCall1.wasDndCheckComputedForCall()).thenReturn(false);
when(mockCall1.getHandle()).thenReturn(Uri.parse(""));
+ // alert the user of the call
when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(true);
- // THEN
+ // run the method under test
assertTrue(mRingerUnderTest.shouldRingForContact(mockCall1));
- verify(mockCall1, atLeastOnce()).setCallIsSuppressedByDoNotDisturb(false);
+
+ // THEN
+ // verify we never set the call object and matchesCallFilter is called
+ verify(mockCall1, never()).setCallIsSuppressedByDoNotDisturb(false);
+ verify(mockNotificationManager, times(1))
+ .matchesCallFilter(any(Bundle.class));
+ }
+
+ /**
+ * ensure Telecom does not re-query the NotificationManager if the call object already has
+ * the result.
+ */
+ @Test
+ public void testShouldRingForContact_matchesCallFilterIsAlreadyComputed() {
+ // WHEN
+ when(mockCall1.wasDndCheckComputedForCall()).thenReturn(true);
+ when(mockCall1.isCallSuppressedByDoNotDisturb()).thenReturn(true);
+
+ // THEN
+ assertFalse(mRingerUnderTest.shouldRingForContact(mockCall1));
+ verify(mockCall1, never()).setCallIsSuppressedByDoNotDisturb(false);
+ verify(mockNotificationManager, never()).matchesCallFilter(any(Bundle.class));
}
@Test
diff --git a/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java b/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
index 346b3d8..c9ea34f 100644
--- a/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
+++ b/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
@@ -197,7 +197,7 @@
ServiceConnection conn2 = captor2.getValue();
conn2.onServiceConnected(mHandle1User1.getComponentName(), service);
- mMonitor.stopFGSDelegation(mHandle1User1);
+ mMonitor.stopFGSDelegation(call1);
verify(mActivityManagerInternal, timeout(TIMEOUT).times(1))
.stopForegroundServiceDelegate(eq(conn2));
conn2.onServiceDisconnected(mHandle1User1.getComponentName());
diff --git a/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java b/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
index e2c7b7b..0a7e27d 100644
--- a/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
+++ b/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
@@ -62,7 +62,7 @@
private int mType;
public TestVoipCallTransaction(String name, long sleepTime, int type) {
- super(mLock);
+ super(VoipCallTransactionTest.this.mLock);
mName = name;
mSleepTime = sleepTime;
mType = type;