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

Commit 4c9d8c1d authored by sqian's avatar sqian
Browse files

Call redirection service processor in Telecom

A single instance of call redirection processor that handles the call
redirection with user-defined CallRedirectionService and carrier
CallRedirectionService for a single call.

A user-defined call redirection will be performed firstly and a carrier
call redirection will be performed after that; there will be a total of
two call redirection cycles.

Bug: 64959558
Test: Compile (will add unit test later)
Change-Id: I9cb08529853bd63a77e8e68b606c79ae3d399cf4
parent af74a5c4
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -137,6 +137,14 @@ public class LogUtils {
        public static final String HANDOVER_FAILED = "HANDOVER_FAILED";
        public static final String START_RINBACK = "START_RINGBACK";
        public static final String STOP_RINGBACK = "STOP_RINGBACK";
        public static final String REDIRECTION_BOUND_USER = "REDIRECTION_BOUND_USER";
        public static final String REDIRECTION_BOUND_CARRIER = "REDIRECTION_BOUND_CARRIER";
        public static final String REDIRECTION_SENT_USER = "REDIRECTION_SENT_USER";
        public static final String REDIRECTION_SENT_CARRIER = "REDIRECTION_SENT_CARRIER";
        public static final String REDIRECTION_COMPLETED_USER = "REDIRECTION_COMPLETED_USER";
        public static final String REDIRECTION_COMPLETED_CARRIER = "REDIRECTION_COMPLETED_CARRIER";
        public static final String REDIRECTION_TIMED_OUT_USER = "REDIRECTION_TIMED_OUT_USER";
        public static final String REDIRECTION_TIMED_OUT_CARRIER = "REDIRECTION_TIMED_OUT_CARRIER";

        public static class Timings {
            public static final String ACCEPT_TIMING = "accept";
+28 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.server.telecom;

import android.content.ContentResolver;
import android.provider.Settings;
import android.telecom.CallRedirectionService;
import java.util.concurrent.TimeUnit;

/**
@@ -49,6 +50,14 @@ public final class Timeouts {
        public long getEmergencyCallbackWindowMillis(ContentResolver cr) {
            return Timeouts.getEmergencyCallbackWindowMillis(cr);
        }

        public long getUserDefinedCallRedirectionTimeoutMillis(ContentResolver cr) {
            return Timeouts.getUserDefinedCallRedirectionTimeoutMillis(cr);
        }

        public long getCarrierCallRedirectionTimeoutMillis(ContentResolver cr) {
            return Timeouts.getCarrierCallRedirectionTimeoutMillis(cr);
        }
    }

    /** A prefix to use for all keys so to not clobber the global namespace. */
@@ -158,4 +167,23 @@ public final class Timeouts {
      return get(contentResolver, "emergency_callback_window_millis",
          TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES));
    }

    /**
     * Returns the amount of time for an user-defined {@link CallRedirectionService}.
     *
     * @param contentResolver The content resolved.
     */
    public static long getUserDefinedCallRedirectionTimeoutMillis(ContentResolver contentResolver) {
        return get(contentResolver, "user_defined_call_redirection_timeout",
            3000L /* 3 seconds */);
    }

    /**
     * Returns the amount of time for a carrier {@link CallRedirectionService}.
     *
     * @param contentResolver The content resolved.
     */
    public static long getCarrierCallRedirectionTimeoutMillis(ContentResolver contentResolver) {
        return get(contentResolver, "carrier_call_redirection_timeout", 3000L /* 3 seconds */);
    }
}
+23 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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.callredirection;

import com.android.server.telecom.Call;

public interface CallRedirectionCallback {
    void onCallRedirectionComplete(Call call);
}
+409 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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.callredirection;

import android.Manifest;
import android.app.AppOpsManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.PersistableBundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.telecom.CallRedirectionService;
import android.telecom.Log;
import android.telecom.Logging.Runnable;
import android.telecom.PhoneAccountHandle;
import android.telephony.CarrierConfigManager;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telecom.ICallRedirectionAdapter;
import com.android.internal.telecom.ICallRedirectionService;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.LogUtils;
import com.android.server.telecom.PhoneAccountRegistrar;
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.Timeouts;

import java.util.List;

/**
 * A single instance of call redirection processor that handles the call redirection with
 * user-defined {@link CallRedirectionService} and carrier {@link CallRedirectionService} for a
 * single call.
 *
 * A user-defined call redirection will be performed firstly and a carrier call redirection will be
 * performed after that; there will be a total of two call redirection cycles.
 *
 * A call redirection cycle is a cycle:
 * 1) Telecom requests a call redirection of a call with a specific {@link CallRedirectionService},
 * 2) Telecom receives the response either from a specific {@link CallRedirectionService} or from
 * the timeout.
 *
 * Telecom should return to {@link CallsManager} at the end of current call redirection
 * cycle, if
 * 1) {@link CallRedirectionService} sends {@link CallRedirectionService#cancelCall()} response
 * before timeout;
 * or 2) Telecom finishes call redirection with carrier {@link CallRedirectionService}.
 */
public class CallRedirectionProcessor implements CallRedirectionCallback {

    private class CallRedirectionAttempt {
        private final ComponentName mComponentName;
        private final String mServiceType;
        private ServiceConnection mConnection;
        private ICallRedirectionService mService;

        private CallRedirectionAttempt(ComponentName componentName, String serviceType) {
            mComponentName = componentName;
            mServiceType = serviceType;
        }

        private void process() {
            Intent intent = new Intent(CallRedirectionService.SERVICE_INTERFACE)
                    .setComponent(mComponentName);
            ServiceConnection connection = new CallRedirectionServiceConnection();
            if (mContext.bindServiceAsUser(
                    intent,
                    connection,
                    Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
                    UserHandle.CURRENT)) {
                Log.d(this, "bindService, found " + mServiceType + " call redirection service,"
                        + " waiting for it to connect");
                mConnection = connection;
            }
        }

        private void onServiceBound(ICallRedirectionService service) {
            mService = service;
            try {
                mService.placeCall(new CallRedirectionAdapter(), mHandle, mPhoneAccountHandle);
                Log.addEvent(mCall, mServiceType.equals(SERVICE_TYPE_USER_DEFINED)
                        ? LogUtils.Events.REDIRECTION_SENT_USER
                        : LogUtils.Events.REDIRECTION_SENT_CARRIER);
                Log.d(this, "Requested placeCall with [handle]" + Log.pii(mHandle)
                        + " [phoneAccountHandle]" + mPhoneAccountHandle);
            } catch (RemoteException e) {
                Log.e(this, e, "Failed to request with the found " + mServiceType + " call"
                        + " redirection service");
                finishCallRedirection();
            }
        }

        private void finishCallRedirection() {
            if (((mServiceType.equals(SERVICE_TYPE_CARRIER)) && mIsCarrierRedirectionPending)
                || ((mServiceType.equals(SERVICE_TYPE_USER_DEFINED))
                    && mIsUserDefinedRedirectionPending)) {
                if (mConnection != null) {
                    // We still need to call unbind even if the service disconnected.
                    mContext.unbindService(mConnection);
                    mConnection = null;
                }
                mService = null;
                onCallRedirectionComplete(mCall);
            }
        }

        private class CallRedirectionServiceConnection implements ServiceConnection {
            @Override
            public void onServiceConnected(ComponentName componentName, IBinder service) {
                Log.startSession("CRSC.oSC");
                try {
                    synchronized (mTelecomLock) {
                        Log.addEvent(mCall, mServiceType.equals(SERVICE_TYPE_USER_DEFINED)
                                ? LogUtils.Events.REDIRECTION_BOUND_USER
                                : LogUtils.Events.REDIRECTION_BOUND_CARRIER, componentName);
                        onServiceBound(ICallRedirectionService.Stub.asInterface(service));
                    }
                } finally {
                    Log.endSession();
                }
            }

            @Override
            public void onServiceDisconnected(ComponentName componentName) {
                Log.startSession("CRSC.oSD");
                try {
                    synchronized (mTelecomLock) {
                        finishCallRedirection();
                    }
                } finally {
                    Log.endSession();
                }
            }
        }

        private class CallRedirectionAdapter extends ICallRedirectionAdapter.Stub {
            @Override
            public void cancelCall() {
                Log.startSession("CRA.cC");
                long token = Binder.clearCallingIdentity();
                try {
                    synchronized (mTelecomLock) {
                        Log.d(this, "Received cancelCall from " +  mServiceType + " call"
                                + " redirection service");
                        mShouldCancelCall = true;
                        finishCallRedirection();
                    }
                } finally {
                    Binder.restoreCallingIdentity(token);
                    Log.endSession();
                }
            }

            @Override
            public void placeCallUnmodified() {
                Log.startSession("CRA.pCU");
                long token = Binder.clearCallingIdentity();
                try {
                    synchronized (mTelecomLock) {
                        Log.d(this, "Received placeCallUnmodified from " +  mServiceType + " call"
                                + " redirection service");
                        finishCallRedirection();
                    }
                } finally {
                    Binder.restoreCallingIdentity(token);
                    Log.endSession();
                }
            }

            @Override
            public void redirectCall(Uri handle, PhoneAccountHandle targetPhoneAccount) {
                Log.startSession("CRA.rC");
                long token = Binder.clearCallingIdentity();
                try {
                    synchronized (mTelecomLock) {
                        mHandle = handle;
                        mPhoneAccountHandle = targetPhoneAccount;
                        Log.d(this, "Received redirectCall with [handle]" + Log.pii(mHandle)
                                + " [phoneAccountHandle]" + mPhoneAccountHandle + " from "
                                + mServiceType + " call" + " redirection service");
                        finishCallRedirection();
                    }
                } finally {
                    Binder.restoreCallingIdentity(token);
                    Log.endSession();
                }
            }
        }
    }

    private final Context mContext;
    private final CallsManager mCallsManager;
    private final Call mCall;
    private final PhoneAccountRegistrar mPhoneAccountRegistrar;
    private final Timeouts.Adapter mTimeoutsAdapter;
    private final TelecomSystem.SyncRoot mTelecomLock;
    private final Handler mHandler = new Handler(Looper.getMainLooper());

    private CallRedirectionAttempt mAttempt;
    public static final String SERVICE_TYPE_CARRIER = "carrier";
    public static final String SERVICE_TYPE_USER_DEFINED = "user_defined";

    private PhoneAccountHandle mPhoneAccountHandle;
    private Uri mHandle;

    /**
     * Indicates if Telecom should cancel the call when the whole call redirection finishes.
     */
    private boolean mShouldCancelCall = false;
    /**
     * Indicates if Telecom is waiting for a callback from a user-defined
     * {@link CallRedirectionService}.
     */
    private boolean mIsUserDefinedRedirectionPending = false;
    /**
     * Indicates if Telecom is waiting for a callback from a carrier
     * {@link CallRedirectionService}.
     */
    private boolean mIsCarrierRedirectionPending = false;

    public CallRedirectionProcessor(
            Context context,
            CallsManager callsManager,
            Call call,
            PhoneAccountRegistrar phoneAccountRegistrar,
            Uri handle,
            PhoneAccountHandle phoneAccountHandle,
            Timeouts.Adapter timeoutsAdapter,
            TelecomSystem.SyncRoot lock) {
        mContext = context;
        mCallsManager = callsManager;
        mCall = call;
        mPhoneAccountRegistrar = phoneAccountRegistrar;
        mHandle = handle;
        mPhoneAccountHandle = phoneAccountHandle;
        mTimeoutsAdapter = timeoutsAdapter;
        mTelecomLock = lock;
    }

    @Override
    public void onCallRedirectionComplete(Call call) {
        // synchronized on mTelecomLock to enter into Telecom.
        mHandler.post(new Runnable("CRP.oCRC", mTelecomLock) {
            @Override
            public void loggedRun() {
                if (mIsUserDefinedRedirectionPending) {
                    Log.addEvent(mCall, LogUtils.Events.REDIRECTION_COMPLETED_USER);
                    mIsUserDefinedRedirectionPending = false;
                    if (mShouldCancelCall) {
                        // TODO mCallsManager.onCallRedirectionComplete
                    } else {
                        performCarrierCallRedirection();
                    }
                }
                if (mIsCarrierRedirectionPending) {
                    Log.addEvent(mCall, LogUtils.Events.REDIRECTION_COMPLETED_CARRIER);
                    mIsCarrierRedirectionPending = false;
                    // TODO mCallsManager.onCallRedirectionComplete
                }
            }
        }.prepare());
    }

    /*
     * The entry to perform call redirection of the call from (@link CallsManager)
     */
    public void performCallRedirection() {
        performUserDefinedCallRedirection();
    }

    private void performUserDefinedCallRedirection() {
        Log.d(this, "performUserDefinedCallRedirection");
        ComponentName componentName = getUserDefinedCallRedirectionService(mContext);
        if (componentName != null && canBindToCallRedirectionService(mContext, componentName)) {
            mAttempt = new CallRedirectionAttempt(componentName, SERVICE_TYPE_USER_DEFINED);
            mAttempt.process();
            mIsUserDefinedRedirectionPending = true;
            processTimeoutForCallRedirection(SERVICE_TYPE_USER_DEFINED);
        } else {
            Log.i(this, "There are no user-defined call redirection services installed on this"
                    + " device.");
            performCarrierCallRedirection();
        }
    }

    private void performCarrierCallRedirection() {
        Log.d(this, "performCarrierCallRedirection");
        ComponentName componentName = getCarrierCallRedirectionService(
            mContext, mPhoneAccountHandle);
        if (componentName != null && canBindToCallRedirectionService(mContext, componentName)) {
            mAttempt = new CallRedirectionAttempt(componentName, SERVICE_TYPE_CARRIER);
            mAttempt.process();
            mIsCarrierRedirectionPending = true;
            processTimeoutForCallRedirection(SERVICE_TYPE_CARRIER);
        } else {
            Log.i(this, "There are no carrier call redirection services installed on this"
                    + " device.");
            // TODO return to CallsManager.onCallRedirectionComplete
        }
    }

    private void processTimeoutForCallRedirection(String serviceType) {
        long timeout = serviceType.equals(SERVICE_TYPE_USER_DEFINED) ?
            mTimeoutsAdapter.getUserDefinedCallRedirectionTimeoutMillis(
                mContext.getContentResolver()) : mTimeoutsAdapter
            .getCarrierCallRedirectionTimeoutMillis(mContext.getContentResolver());

        mHandler.postDelayed(new Runnable("CRP.pTFCR", null) {
            @Override
            public void loggedRun() {
                boolean isCurrentRedirectionPending =
                        serviceType.equals(SERVICE_TYPE_USER_DEFINED) ?
                                mIsUserDefinedRedirectionPending : mIsCarrierRedirectionPending;
                if (isCurrentRedirectionPending) {
                    Log.i(CallRedirectionProcessor.this,
                            serviceType + "call redirection has timed out.");
                    Log.addEvent(mCall, serviceType.equals(SERVICE_TYPE_USER_DEFINED)
                            ? LogUtils.Events.REDIRECTION_TIMED_OUT_USER
                            : LogUtils.Events.REDIRECTION_TIMED_OUT_CARRIER);
                    onCallRedirectionComplete(mCall);
                }
            }
        }.prepare(), timeout);
    }

    private ComponentName getUserDefinedCallRedirectionService(Context context) {
        // TODO get service component name from settings default value:
        // android.provider.Settings#CALL_REDIRECTION_DEFAULT_APPLICATION
        return null;
    }

    private ComponentName getCarrierCallRedirectionService(Context context, PhoneAccountHandle
            targetPhoneAccountHandle) {
        CarrierConfigManager configManager = (CarrierConfigManager)
                context.getSystemService(Context.CARRIER_CONFIG_SERVICE);
        if (configManager == null) {
            Log.i(this, "Cannot get CarrierConfigManager.");
            return null;
        }
        PersistableBundle pb = configManager.getConfigForSubId(mPhoneAccountRegistrar
                .getSubscriptionIdForPhoneAccount(targetPhoneAccountHandle));
        if (pb == null) {
            Log.i(this, "Cannot get PersistableBundle.");
            return null;
        }
        String componentNameString = pb.getString(
            CarrierConfigManager.KEY_CALL_REDIRECTION_SERVICE_COMPONENT_NAME_STRING);
        return new ComponentName(context, componentNameString);
    }

    private boolean canBindToCallRedirectionService(Context context, ComponentName componentName) {
        Intent intent = new Intent(CallRedirectionService.SERVICE_INTERFACE);
        intent.setComponent(componentName);
        List<ResolveInfo> entries = mContext.getPackageManager().queryIntentServicesAsUser(
                intent, 0, mCallsManager.getCurrentUserHandle().getIdentifier());
        if (entries.isEmpty()) {
            Log.i(this, "There are no call redirection services installed on this device.");
            return false;
        } else if (entries.size() != 1) {
            Log.i(this, "There are multiple call redirection services installed on this device.");
            return false;
        } else {
            ResolveInfo entry = entries.get(0);
            if (entry.serviceInfo.permission == null || !entry.serviceInfo.permission.equals(
                    Manifest.permission.BIND_CALL_REDIRECTION_SERVICE)) {
                Log.w(this, "CallRedirectionService must require BIND_CALL_REDIRECTION_SERVICE"
                        + " permission: " + entry.serviceInfo.packageName);
                return false;
            }
            AppOpsManager appOps = (AppOpsManager) context.getSystemService(
                Context.APP_OPS_SERVICE);
            if (appOps.noteOp(AppOpsManager.OP_PROCESS_OUTGOING_CALLS, Binder.getCallingUid(),
                    entry.serviceInfo.packageName) != AppOpsManager.MODE_ALLOWED) {
                Log.w(this, "App Ops does not allow " + entry.serviceInfo.packageName);
                return false;
            }
        }
        return true;
    }

    /**
     * Returns the handler for testing purposes.
     */
    @VisibleForTesting
    public Handler getHandler() {
        return mHandler;
    }
}