Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 754dc5de authored by Tyler Gunn's avatar Tyler Gunn
Browse files

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
Merged-In: I8539b0601cf5f324d2fb204485ee0d9bbf03426d
Change-Id: I8539b0601cf5f324d2fb204485ee0d9bbf03426d
parent d62e9610
Loading
Loading
Loading
Loading
+10 −1
Original line number Diff line number Diff line
@@ -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"
+4 −0
Original line number Diff line number Diff line
@@ -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>
+15 −0
Original line number Diff line number Diff line
@@ -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;
@@ -310,6 +311,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
@@ -1706,6 +1713,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.
     */
+23 −18
Original line number Diff line number Diff line
@@ -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,14 +137,23 @@ 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) {
            sendNewOutgoingCallIntent(context, call, callsManager, intent);
        }
    }

    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);
@@ -158,7 +164,6 @@ public class CallIntentProcessor {
            disconnectCallAndShowErrorDialog(context, call, result);
        }
    }
    }

    /**
     * If the call is initiated from managed profile but there is no work dialer installed, treat
+142 −19
Original line number Diff line number Diff line
@@ -66,6 +66,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;
@@ -208,6 +209,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
@@ -949,15 +956,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);

@@ -997,7 +1004,6 @@ public class CallsManager extends Call.ListenerBase
            }

            call.setInitiatingUser(initiatingUser);

            isReusedCall = false;
        }

@@ -1115,9 +1121,15 @@ public class CallsManager extends Call.ListenerBase
        }
        setIntentExtrasAndStartTime(call, extras);

        if ((isPotentialMMICode(handle) || isPotentialInCallMMICode)
                && !needsAccountSelection) {
            // Do not add the call if it is a potential MMI code.
        if ((isPotentialMMICode(handle) || isPotentialInCallMMICode) && !needsAccountSelection) {
            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}).
@@ -1190,23 +1202,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);
@@ -1702,6 +1702,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.
@@ -2659,6 +2686,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}.
     *
@@ -2675,6 +2793,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();
Loading