Confirm managed call when there are ongoing self-managed calls.

When the user places a managed call while there are ongoing self-managed
calls, the system will now display a dialog giving the user the option of
NOT placing the managed call, or placing the managed call and disconnecting
the ongoing self-managed call(s).

This is done by stopping the outgoing call during startOutgoingCall and
bringing up a dialog to confirm whether the user wants to place the call.
If they chose to place the call, ongoing self-mgds calls are disconnected,
the call is added, and the NewutgoingCallBroadcast is sent as usual.

If the user chooses not to start the call, the call is cancelled.

Test: Manual
Bug: 37828805
Change-Id: I8539b0601cf5f324d2fb204485ee0d9bbf03426d
This commit is contained in:
Tyler Gunn 2017-04-30 14:16:07 -07:00
parent fa1b42cae1
commit bbd78a76f8
8 changed files with 328 additions and 38 deletions

View File

@ -212,6 +212,8 @@
<action android:name="com.android.server.telecom.ACTION_SEND_SMS_FROM_NOTIFICATION" />
<action android:name="com.android.server.telecom.ACTION_ANSWER_FROM_NOTIFICATION" />
<action android:name="com.android.server.telecom.ACTION_REJECT_FROM_NOTIFICATION" />
<action android:name="com.android.server.telecom.PROCEED_WITH_CALL" />
<action android:name="com.android.server.telecom.CANCEL_CALL" />
</intent-filter>
</receiver>
@ -254,6 +256,14 @@
android:process=":ui">
</activity>
<activity android:name=".ui.ConfirmCallDialogActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:excludeFromRecents="true"
android:launchMode="singleInstance"
android:theme="@style/Theme.Telecomm.Transparent"
android:process=":ui">
</activity>
<activity android:name=".components.ChangeDefaultDialerDialog"
android:label="@string/change_default_dialer_dialog_title"
android:excludeFromRecents="true"
@ -265,7 +275,6 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity android:name=".testapps.IncomingSelfManagedCallActivity" />
<receiver android:name=".components.PrimaryCallReceiver"
android:exported="true"

View File

@ -252,4 +252,8 @@
<string name="notification_channel_incoming_call">Incoming calls</string>
<!-- Notification channel name for a channel containing missed call notifications. -->
<string name="notification_channel_missed_call">Missed calls</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". -->
<string name="alert_outgoing_call">Placing this call will end your <xliff:g id="other_app">%1$s</xliff:g> call.</string>
</resources>

View File

@ -17,6 +17,7 @@
package com.android.server.telecom;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
@ -317,6 +318,12 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable {
private Bundle mIntentExtras = new Bundle();
/**
* The {@link Intent} which originally created this call. Only populated when we are putting a
* call into a pending state and need to pick up initiation of the call later.
*/
private Intent mOriginalCallIntent = null;
/** Set of listeners on this call.
*
* ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
@ -1786,6 +1793,14 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable {
mIntentExtras = extras;
}
public Intent getOriginalCallIntent() {
return mOriginalCallIntent;
}
public void setOriginalCallIntent(Intent intent) {
mOriginalCallIntent = intent;
}
/**
* @return the uri of the contact associated with this call.
*/

View File

@ -9,7 +9,6 @@ import android.os.Bundle;
import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
import android.telecom.Connection;
import android.telecom.DefaultDialerManager;
import android.telecom.Log;
import android.telecom.PhoneAccount;
@ -128,8 +127,6 @@ public class CallIntentProcessor {
VideoProfile.STATE_AUDIO_ONLY);
clientExtras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, videoState);
final boolean isPrivilegedDialer = intent.getBooleanExtra(KEY_IS_PRIVILEGED_DIALER, false);
boolean fixedInitiatingUser = fixInitiatingUserIfNecessary(context, intent);
// Show the toast to warn user that it is a personal call though initiated in work profile.
if (fixedInitiatingUser) {
@ -140,23 +137,31 @@ public class CallIntentProcessor {
// Send to CallsManager to ensure the InCallUI gets kicked off before the broadcast returns
Call call = callsManager
.startOutgoingCall(handle, phoneAccountHandle, clientExtras, initiatingUser);
.startOutgoingCall(handle, phoneAccountHandle, clientExtras, initiatingUser,
intent);
if (call != null) {
// Asynchronous calls should not usually be made inside a BroadcastReceiver because once
// onReceive is complete, the BroadcastReceiver's process runs the risk of getting
// killed if memory is scarce. However, this is OK here because the entire Telecom
// process will be running throughout the duration of the phone call and should never
// be killed.
NewOutgoingCallIntentBroadcaster broadcaster = new NewOutgoingCallIntentBroadcaster(
context, callsManager, call, intent, callsManager.getPhoneNumberUtilsAdapter(),
isPrivilegedDialer);
final int result = broadcaster.processIntent();
final boolean success = result == DisconnectCause.NOT_DISCONNECTED;
sendNewOutgoingCallIntent(context, call, callsManager, intent);
}
}
if (!success && call != null) {
disconnectCallAndShowErrorDialog(context, call, result);
}
static void sendNewOutgoingCallIntent(Context context, Call call, CallsManager callsManager,
Intent intent) {
// Asynchronous calls should not usually be made inside a BroadcastReceiver because once
// onReceive is complete, the BroadcastReceiver's process runs the risk of getting
// killed if memory is scarce. However, this is OK here because the entire Telecom
// process will be running throughout the duration of the phone call and should never
// be killed.
final boolean isPrivilegedDialer = intent.getBooleanExtra(KEY_IS_PRIVILEGED_DIALER, false);
NewOutgoingCallIntentBroadcaster broadcaster = new NewOutgoingCallIntentBroadcaster(
context, callsManager, call, intent, callsManager.getPhoneNumberUtilsAdapter(),
isPrivilegedDialer);
final int result = broadcaster.processIntent();
final boolean success = result == DisconnectCause.NOT_DISCONNECTED;
if (!success && call != null) {
disconnectCallAndShowErrorDialog(context, call, result);
}
}

View File

@ -65,6 +65,7 @@ import com.android.server.telecom.callfiltering.CallScreeningServiceFilter;
import com.android.server.telecom.callfiltering.DirectToVoicemailCallFilter;
import com.android.server.telecom.callfiltering.IncomingCallFilter;
import com.android.server.telecom.components.ErrorDialogActivity;
import com.android.server.telecom.ui.ConfirmCallDialogActivity;
import com.android.server.telecom.ui.IncomingCallNotifier;
import java.util.ArrayList;
@ -207,6 +208,12 @@ public class CallsManager extends Call.ListenerBase
private final Set<Call> mCalls = Collections.newSetFromMap(
new ConcurrentHashMap<Call, Boolean>(8, 0.9f, 1));
/**
* A pending call is one which requires user-intervention in order to be placed.
* Used by {@link #startCallConfirmation(Call)}.
*/
private Call mPendingCall;
/**
* The current telecom call ID. Used when creating new instances of {@link Call}. Should
* only be accessed using the {@link #getNextCallId()} method which synchronizes on the
@ -994,15 +1001,15 @@ public class CallsManager extends Call.ListenerBase
* For managed connections, this is the first step to launching the Incall UI.
* For self-managed connections, we don't expect the Incall UI to launch, but this is still a
* first step in getting the self-managed ConnectionService to create the connection.
*
* @param handle Handle to connect the call with.
* @param phoneAccountHandle The phone account which contains the component name of the
* connection service to use for this call.
* @param extras The optional extras Bundle passed with the intent used for the incoming call.
* @param initiatingUser {@link UserHandle} of user that place the outgoing call.
* @param originalIntent
*/
Call startOutgoingCall(Uri handle, PhoneAccountHandle phoneAccountHandle, Bundle extras,
UserHandle initiatingUser) {
UserHandle initiatingUser, Intent originalIntent) {
boolean isReusedCall = true;
Call call = reuseOutgoingCall(handle);
@ -1042,7 +1049,6 @@ public class CallsManager extends Call.ListenerBase
}
call.setInitiatingUser(initiatingUser);
isReusedCall = false;
}
@ -1160,9 +1166,15 @@ public class CallsManager extends Call.ListenerBase
}
setIntentExtrasAndStartTime(call, extras);
// Do not add the call if it is a potential MMI code.
if ((isPotentialMMICode(handle) || isPotentialInCallMMICode) && !needsAccountSelection) {
if ((isPotentialMMICode(handle) || isPotentialInCallMMICode)
&& !needsAccountSelection) {
// Do not add the call if it is a potential MMI code.
call.addListener(this);
} else if (!isSelfManaged && hasSelfManagedCalls() && !call.isEmergencyCall()) {
// Adding a managed call and there are ongoing self-managed call(s).
call.setOriginalCallIntent(originalIntent);
startCallConfirmation(call);
return null;
} else if (!mCalls.contains(call)) {
// We check if mCalls already contains the call because we could potentially be reusing
// a call which was previously added (See {@link #reuseOutgoingCall}).
@ -1235,23 +1247,11 @@ public class CallsManager extends Call.ListenerBase
if (call.isSelfManaged() && !isOutgoingCallPermitted) {
notifyCreateConnectionFailed(call.getTargetPhoneAccount(), call);
} else if (!call.isSelfManaged() && hasSelfManagedCalls() && !call.isEmergencyCall()) {
Call activeCall = getActiveCall();
CharSequence errorMessage;
if (activeCall == null) {
// Realistically this shouldn't happen, but best to handle gracefully
errorMessage = mContext.getText(R.string.cant_call_due_to_ongoing_unknown_call);
} else {
errorMessage = mContext.getString(R.string.cant_call_due_to_ongoing_call,
activeCall.getTargetPhoneAccountLabel());
}
// Call is managed and there are ongoing self-managed calls.
markCallAsDisconnected(call, new DisconnectCause(DisconnectCause.ERROR,
errorMessage, errorMessage, "Ongoing call in another app."));
markCallAsRemoved(call);
markCallDisconnectedDueToSelfManagedCall(call);
} else {
if (call.isEmergencyCall()) {
// Disconnect all self-managed calls to make priority for emergency call.
mCalls.stream().filter(c -> c.isSelfManaged()).forEach(c -> c.disconnect());
disconnectSelfManagedCalls();
}
call.startCreateConnection(mPhoneAccountRegistrar);
@ -1749,6 +1749,33 @@ public class CallsManager extends Call.ListenerBase
}
}
/**
* Given a call, marks the call as disconnected and removes it. Set the error message to
* indicate to the user that the call cannot me placed due to an ongoing call in another app.
*
* Used when there are ongoing self-managed calls and the user tries to make an outgoing managed
* call. Called by {@link #startCallConfirmation(Call)} when the user is already confirming an
* outgoing call. Realistically this should almost never be called since in practice the user
* won't make multiple outgoing calls at the same time.
*
* @param call The call to mark as disconnected.
*/
void markCallDisconnectedDueToSelfManagedCall(Call call) {
Call activeCall = getActiveCall();
CharSequence errorMessage;
if (activeCall == null) {
// Realistically this shouldn't happen, but best to handle gracefully
errorMessage = mContext.getText(R.string.cant_call_due_to_ongoing_unknown_call);
} else {
errorMessage = mContext.getString(R.string.cant_call_due_to_ongoing_call,
activeCall.getTargetPhoneAccountLabel());
}
// Call is managed and there are ongoing self-managed calls.
markCallAsDisconnected(call, new DisconnectCause(DisconnectCause.ERROR,
errorMessage, errorMessage, "Ongoing call in another app."));
markCallAsRemoved(call);
}
/**
* Cleans up any calls currently associated with the specified connection service when the
* service binder disconnects unexpectedly.
@ -2760,6 +2787,97 @@ public class CallsManager extends Call.ListenerBase
}
}
/**
* Used to confirm creation of an outgoing call which was marked as pending confirmation in
* {@link #startOutgoingCall(Uri, PhoneAccountHandle, Bundle, UserHandle, Intent)}.
* Called via {@link TelecomBroadcastIntentProcessor} for a call which was confirmed via
* {@link ConfirmCallDialogActivity}.
* @param callId The call ID of the call to confirm.
*/
public void confirmPendingCall(String callId) {
Log.i(this, "confirmPendingCall: callId=%s", callId);
if (mPendingCall != null && mPendingCall.getId().equals(callId)) {
Log.addEvent(mPendingCall, LogUtils.Events.USER_CONFIRMED);
addCall(mPendingCall);
// We are going to place the new outgoing call, so disconnect any ongoing self-managed
// calls which are ongoing at this time.
disconnectSelfManagedCalls();
// Kick of the new outgoing call intent from where it left off prior to confirming the
// call.
CallIntentProcessor.sendNewOutgoingCallIntent(mContext, mPendingCall, this,
mPendingCall.getOriginalCallIntent());
mPendingCall = null;
}
}
/**
* Used to cancel an outgoing call which was marked as pending confirmation in
* {@link #startOutgoingCall(Uri, PhoneAccountHandle, Bundle, UserHandle, Intent)}.
* Called via {@link TelecomBroadcastIntentProcessor} for a call which was confirmed via
* {@link ConfirmCallDialogActivity}.
* @param callId The call ID of the call to cancel.
*/
public void cancelPendingCall(String callId) {
Log.i(this, "cancelPendingCall: callId=%s", callId);
if (mPendingCall != null && mPendingCall.getId().equals(callId)) {
Log.addEvent(mPendingCall, LogUtils.Events.USER_CANCELLED);
markCallAsDisconnected(mPendingCall, new DisconnectCause(DisconnectCause.CANCELED));
markCallAsRemoved(mPendingCall);
mPendingCall = null;
}
}
/**
* Called from {@link #startOutgoingCall(Uri, PhoneAccountHandle, Bundle, UserHandle, Intent)} when
* a managed call is added while there are ongoing self-managed calls. Starts
* {@link ConfirmCallDialogActivity} to prompt the user to see if they wish to place the
* outgoing call or not.
* @param call The call to confirm.
*/
private void startCallConfirmation(Call call) {
if (mPendingCall != null) {
Log.i(this, "startCallConfirmation: call %s is already pending; disconnecting %s",
mPendingCall.getId(), call.getId());
markCallDisconnectedDueToSelfManagedCall(call);
return;
}
Log.addEvent(call, LogUtils.Events.USER_CONFIRMATION);
mPendingCall = call;
// Figure out the name of the app in charge of the self-managed call(s).
Call selfManagedCall = mCalls.stream()
.filter(c -> c.isSelfManaged())
.findFirst()
.orElse(null);
CharSequence ongoingAppName = "";
if (selfManagedCall != null) {
ongoingAppName = selfManagedCall.getTargetPhoneAccountLabel();
}
Log.i(this, "startCallConfirmation: callId=%s, ongoingApp=%s", call.getId(),
ongoingAppName);
Intent confirmIntent = new Intent(mContext, ConfirmCallDialogActivity.class);
confirmIntent.putExtra(ConfirmCallDialogActivity.EXTRA_OUTGOING_CALL_ID, call.getId());
confirmIntent.putExtra(ConfirmCallDialogActivity.EXTRA_ONGOING_APP_NAME, ongoingAppName);
confirmIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivityAsUser(confirmIntent, UserHandle.CURRENT);
}
/**
* Disconnects all self-managed calls.
*/
private void disconnectSelfManagedCalls() {
// Disconnect all self-managed calls to make priority for emergency call.
// Use Call.disconnect() to command the ConnectionService to disconnect the calls.
// CallsManager.markCallAsDisconnected doesn't actually tell the ConnectionService to
// disconnect.
mCalls.stream()
.filter(c -> c.isSelfManaged())
.forEach(c -> c.disconnect());
}
/**
* Dumps the state of the {@link CallsManager}.
*
@ -2776,6 +2894,11 @@ public class CallsManager extends Call.ListenerBase
pw.decreaseIndent();
}
if (mPendingCall != null) {
pw.print("mPendingCall:");
pw.println(mPendingCall.getId());
}
if (mCallAudioManager != null) {
pw.println("mCallAudioManager:");
pw.increaseIndent();
@ -2903,7 +3026,7 @@ public class CallsManager extends Call.ListenerBase
extras.putParcelable(TelecomManager.EXTRA_CALL_AUDIO_STATE,
mCallAudioManager.getCallAudioState());
Call handoverToCall = startOutgoingCall(handoverFromCall.getHandle(), handoverToHandle,
extras, getCurrentUserHandle());
extras, getCurrentUserHandle(), null /* originalIntent */);
Log.addEvent(handoverFromCall, LogUtils.Events.START_HANDOVER,
"handOverFrom=%s, handOverTo=%s", handoverFromCall.getId(), handoverToCall.getId());
handoverFromCall.setHandoverToCall(handoverToCall);

View File

@ -54,6 +54,9 @@ public class LogUtils {
public final static class Events {
public static final String CREATED = "CREATED";
public static final String USER_CONFIRMATION = "USER_CONFIRMATION";
public static final String USER_CONFIRMED = "USER_CONFIRMED";
public static final String USER_CANCELLED = "USER_CANCELLED";
public static final String DESTROYED = "DESTROYED";
public static final String SET_CONNECTING = "SET_CONNECTING";
public static final String SET_DIALING = "SET_DIALING";

View File

@ -21,6 +21,8 @@ import android.content.Intent;
import android.os.UserHandle;
import android.telecom.Log;
import com.android.server.telecom.ui.ConfirmCallDialogActivity;
public final class TelecomBroadcastIntentProcessor {
/** The action used to send SMS response for the missed call notification. */
public static final String ACTION_SEND_SMS_FROM_NOTIFICATION =
@ -48,6 +50,20 @@ public final class TelecomBroadcastIntentProcessor {
public static final String ACTION_REJECT_FROM_NOTIFICATION =
"com.android.server.telecom.ACTION_REJECT_FROM_NOTIFICATION";
/**
* The action used to proceed with a call being confirmed via
* {@link com.android.server.telecom.ui.ConfirmCallDialogActivity}.
*/
public static final String ACTION_PROCEED_WITH_CALL =
"com.android.server.telecom.PROCEED_WITH_CALL";
/**
* The action used to cancel a call being confirmed via
* {@link com.android.server.telecom.ui.ConfirmCallDialogActivity}.
*/
public static final String ACTION_CANCEL_CALL =
"com.android.server.telecom.CANCEL_CALL";
public static final String EXTRA_USERHANDLE = "userhandle";
private final Context mContext;
@ -112,6 +128,7 @@ public final class TelecomBroadcastIntentProcessor {
} else if (ACTION_REJECT_FROM_NOTIFICATION.equals(action)) {
Log.startSession("TBIP.aRFM");
try {
// Reject the current ringing call.
Call incomingCall = mCallsManager.getIncomingCallNotifier().getIncomingCall();
if (incomingCall != null) {
@ -120,6 +137,24 @@ public final class TelecomBroadcastIntentProcessor {
} finally {
Log.endSession();
}
} else if (ACTION_PROCEED_WITH_CALL.equals(action)) {
Log.startSession("TBIP.aPWC");
try {
String callId = intent.getStringExtra(
ConfirmCallDialogActivity.EXTRA_OUTGOING_CALL_ID);
mCallsManager.confirmPendingCall(callId);
} finally {
Log.endSession();
}
} else if (ACTION_CANCEL_CALL.equals(action)) {
Log.startSession("TBIP.aCC");
try {
String callId = intent.getStringExtra(
ConfirmCallDialogActivity.EXTRA_OUTGOING_CALL_ID);
mCallsManager.cancelPendingCall(callId);
} finally {
Log.endSession();
}
}
}

View File

@ -0,0 +1,96 @@
/*
* Copyright (C) 2017 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 com.android.server.telecom.R;
import com.android.server.telecom.TelecomBroadcastIntentProcessor;
import com.android.server.telecom.components.TelecomBroadcastReceiver;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.telecom.Log;
/**
* Dialog activity used when there is an ongoing self-managed call and the user initiates a new
* outgoing managed call. The dialog prompts the user to see if they want to disconnect the ongoing
* self-managed call in order to place the new managed call.
*/
public class ConfirmCallDialogActivity extends Activity {
public static final String EXTRA_OUTGOING_CALL_ID = "android.telecom.extra.OUTGOING_CALL_ID";
public static final String EXTRA_ONGOING_APP_NAME = "android.telecom.extra.ONGOING_APP_NAME";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final String callId = getIntent().getStringExtra(EXTRA_OUTGOING_CALL_ID);
final CharSequence ongoingAppName = getIntent().getCharSequenceExtra(
EXTRA_ONGOING_APP_NAME);
showDialog(callId, ongoingAppName);
}
private void showDialog(final String callId, CharSequence ongoingAppName) {
Log.i(this, "showDialog: confirming callId=%s, ongoing=%s", callId, ongoingAppName);
CharSequence message = getString(R.string.alert_outgoing_call, ongoingAppName);
final AlertDialog errorDialog = new AlertDialog.Builder(this)
.setMessage(message)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent proceedWithCall = new Intent(
TelecomBroadcastIntentProcessor.ACTION_PROCEED_WITH_CALL, null,
ConfirmCallDialogActivity.this,
TelecomBroadcastReceiver.class);
proceedWithCall.putExtra(EXTRA_OUTGOING_CALL_ID, callId);
sendBroadcast(proceedWithCall);
dialog.dismiss();
finish();
}
})
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent cancelCall = new Intent(
TelecomBroadcastIntentProcessor.ACTION_CANCEL_CALL, null,
ConfirmCallDialogActivity.this,
TelecomBroadcastReceiver.class);
cancelCall.putExtra(EXTRA_OUTGOING_CALL_ID, callId);
sendBroadcast(cancelCall);
dialog.dismiss();
finish();
}
})
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
Intent cancelCall = new Intent(
TelecomBroadcastIntentProcessor.ACTION_CANCEL_CALL, null,
ConfirmCallDialogActivity.this,
TelecomBroadcastReceiver.class);
cancelCall.putExtra(EXTRA_OUTGOING_CALL_ID, callId);
sendBroadcast(cancelCall);
dialog.dismiss();
finish();
}
})
.create();
errorDialog.show();
}
}