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

Commit 6fdc9932 authored by Youming Ye's avatar Youming Ye
Browse files

Enable Telecom to bind third party InCallServices.

Changes for Telecom to bind third party companion apps or
automotive ui installed from Play Store. User can select the app
to bind using Settings.

Bug: 78174835
Test: Manual
Change-Id: Ife293b252c1b7a8ff07b4f4f22bacfa0c3df6195
Merged-In: Ife293b252c1b7a8ff07b4f4f22bacfa0c3df6195
parent 5fb84993
Loading
Loading
Loading
Loading
+33 −8
Original line number Diff line number Diff line
@@ -34,7 +34,6 @@ import android.os.Trace;
import android.os.UserHandle;
import android.telecom.CallAudioState;
import android.telecom.ConnectionService;
import android.telecom.DefaultDialerManager;
import android.telecom.InCallService;
import android.telecom.Log;
import android.telecom.Logging.Runnable;
@@ -717,6 +716,7 @@ public class InCallController extends CallsManagerListenerBase {
    private static final int IN_CALL_SERVICE_TYPE_SYSTEM_UI = 2;
    private static final int IN_CALL_SERVICE_TYPE_CAR_MODE_UI = 3;
    private static final int IN_CALL_SERVICE_TYPE_NON_UI = 4;
    private static final int IN_CALL_SERVICE_TYPE_COMPANION = 5;

    /** The in-call app implementations, see {@link IInCallService}. */
    private final Map<InCallServiceInfo, IInCallService> mInCallServices = new ArrayMap<>();
@@ -1146,6 +1146,17 @@ public class InCallController extends CallsManagerListenerBase {
        for (InCallServiceInfo serviceInfo : nonUIInCallComponents) {
            nonUIInCalls.add(new InCallServiceBindingConnection(serviceInfo));
        }
        List<String> callCompanionApps = mCallsManager
                .getRoleManagerAdapter().getCallCompanionApps();
        if (callCompanionApps != null && !callCompanionApps.isEmpty()) {
            for(String pkg : callCompanionApps) {
                InCallServiceInfo info = getInCallServiceComponent(pkg,
                        IN_CALL_SERVICE_TYPE_COMPANION);
                if (info != null) {
                    nonUIInCalls.add(new InCallServiceBindingConnection(info));
                }
            }
        }
        mNonUIInCallServiceConnections = new NonUIInCallServiceConnectionCollection(nonUIInCalls);
        mNonUIInCallServiceConnections.connect(call);
    }
@@ -1159,9 +1170,10 @@ public class InCallController extends CallsManagerListenerBase {
    }

    private InCallServiceInfo getCarModeComponent() {
        // Seems strange to cast a String to null, but the signatures of getInCallServiceComponent
        // differ in the types of the first parameter, and passing in null is inherently ambiguous.
        return getInCallServiceComponent((String) null, IN_CALL_SERVICE_TYPE_CAR_MODE_UI);
        // The signatures of getInCallServiceComponent differ in the types of the first parameter,
        // and passing in null is inherently ambiguous. (If no car mode component found)
        String defaultCarMode = mCallsManager.getRoleManagerAdapter().getCarModeDialerApp();
        return getInCallServiceComponent(defaultCarMode, IN_CALL_SERVICE_TYPE_CAR_MODE_UI);
    }

    private InCallServiceInfo getInCallServiceComponent(ComponentName componentName, int type) {
@@ -1216,7 +1228,6 @@ public class InCallController extends CallsManagerListenerBase {
                PackageManager.GET_META_DATA,
                mCallsManager.getCurrentUserHandle().getIdentifier())) {
            ServiceInfo serviceInfo = entry.serviceInfo;

            if (serviceInfo != null) {
                boolean isExternalCallsSupported = serviceInfo.metaData != null &&
                        serviceInfo.metaData.getBoolean(
@@ -1266,6 +1277,14 @@ public class InCallController extends CallsManagerListenerBase {
            return IN_CALL_SERVICE_TYPE_SYSTEM_UI;
        }

        // Check to see if the service holds permissions or metadata for third party apps.
        boolean hasInCallServiceUIMetadata = serviceInfo.metaData != null &&
                serviceInfo.metaData.containsKey(TelecomManager.METADATA_IN_CALL_SERVICE_UI);
        boolean isThirdPartyCompanionApp = packageManager.checkPermission(
                Manifest.permission.CALL_COMPANION_APP,
                serviceInfo.packageName) == PackageManager.PERMISSION_GRANTED &&
                !hasInCallServiceUIMetadata;

        // Check to see if the service is a car-mode UI type by checking that it has the
        // CONTROL_INCALL_EXPERIENCE (to verify it is a system app) and that it has the
        // car-mode UI metadata.
@@ -1275,7 +1294,7 @@ public class InCallController extends CallsManagerListenerBase {
        boolean isCarModeUIService = serviceInfo.metaData != null &&
                serviceInfo.metaData.getBoolean(
                        TelecomManager.METADATA_IN_CALL_SERVICE_CAR_MODE_UI, false) &&
                hasControlInCallPermission;
                (hasControlInCallPermission || isThirdPartyCompanionApp);
        if (isCarModeUIService) {
            return IN_CALL_SERVICE_TYPE_CAR_MODE_UI;
        }
@@ -1293,9 +1312,15 @@ public class InCallController extends CallsManagerListenerBase {

        // Also allow any in-call service that has the control-experience permission (to ensure
        // that it is a system app) and doesn't claim to show any UI.
        if (hasControlInCallPermission && !isUIService) {
        if (!isUIService) {
            if (hasControlInCallPermission && !isThirdPartyCompanionApp) {
                return IN_CALL_SERVICE_TYPE_NON_UI;
            }
            // Third party companion alls without CONTROL_INCALL_EXPERIENCE permission.
            if (!hasControlInCallPermission && isThirdPartyCompanionApp) {
                return IN_CALL_SERVICE_TYPE_COMPANION;
            }
        }

        // Anything else that remains, we will not bind to.
        Log.i(this, "Skipping binding to %s:%s, control: %b, car-mode: %b, ui: %b",
+3 −1
Original line number Diff line number Diff line
@@ -73,7 +73,9 @@ public class RoleManagerAdapterImpl implements RoleManagerAdapter {

    @Override
    public List<String> getCallCompanionApps() {
        List<String> callCompanionApps = getRoleManagerCallCompanionApps();
        List<String> callCompanionApps = new ArrayList<>();
        // List from RoleManager is not resizable. AbstractList.add action is not supported.
        callCompanionApps.addAll(getRoleManagerCallCompanionApps());
        callCompanionApps.addAll(mOverrideCallCompanionApps);
        return callCompanionApps;
    }
+12 −0
Original line number Diff line number Diff line
@@ -26,9 +26,11 @@ import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import android.Manifest;
import android.app.AppOpsManager;
import android.app.NotificationManager;
import android.app.StatusBarManager;
// import android.app.role.RoleManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver;
@@ -71,6 +73,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;

import static org.mockito.ArgumentMatchers.matches;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
@@ -181,6 +184,9 @@ public class ComponentContextFixture implements TestFixture<Context> {
                    return mCarrierConfigManager;
                case Context.COUNTRY_DETECTOR:
                    return mCountryDetector;
                // TODO: RoleManager not ready yet, uncomment when it's merged into aosp.
                // case Context.ROLE_SERVICE:
                //     return mRoleManager;
                default:
                    return null;
            }
@@ -432,6 +438,7 @@ public class ComponentContextFixture implements TestFixture<Context> {
    private final Map<String, IContentProvider> mIContentProviderByUri = new HashMap<>();
    private final Configuration mResourceConfiguration = new Configuration();
    private final ApplicationInfo mTestApplicationInfo = new ApplicationInfo();
    // private final RoleManager mRoleManager = mock(RoleManager.class);

    private TelecomManager mTelecomManager = null;

@@ -463,6 +470,11 @@ public class ComponentContextFixture implements TestFixture<Context> {
            }
        }).when(mPackageManager).queryIntentServicesAsUser((Intent) any(), anyInt(), anyInt());

        // By default, tests use non-ui apps instead of 3rd party companion apps.
        when(mPackageManager.checkPermission(
                matches(Manifest.permission.CALL_COMPANION_APP), anyString()))
                .thenReturn(PackageManager.PERMISSION_DENIED);

        when(mTelephonyManager.getSubIdForPhoneAccount((PhoneAccount) any())).thenReturn(1);

        when(mTelephonyManager.getNetworkOperatorName()).thenReturn("label1");
+150 −4
Original line number Diff line number Diff line
@@ -50,6 +50,7 @@ import com.android.server.telecom.EmergencyCallHelper;
import com.android.server.telecom.InCallController;
import com.android.server.telecom.PhoneAccountRegistrar;
import com.android.server.telecom.R;
import com.android.server.telecom.RoleManagerAdapter;
import com.android.server.telecom.SystemStateHelper;
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.Timeouts;
@@ -65,11 +66,15 @@ import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.matches;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
@@ -95,12 +100,17 @@ public class InCallControllerTests extends TelecomTestCase {
    @Mock MockContext mMockContext;
    @Mock Timeouts.Adapter mTimeoutsAdapter;
    @Mock DefaultDialerCache mDefaultDialerCache;
    @Mock RoleManagerAdapter mMockRoleManagerAdapter;

    private static final int CURRENT_USER_ID = 900973;
    private static final String DEF_PKG = "defpkg";
    private static final String DEF_CLASS = "defcls";
    private static final String SYS_PKG = "syspkg";
    private static final String SYS_CLASS = "syscls";
    private static final String COMPANION_PKG = "cpnpkg";
    private static final String COMPANION_CLASS = "cpncls";
    private static final String CAR_PKG = "carpkg";
    private static final String CAR_CLASS = "carcls";
    private static final PhoneAccountHandle PA_HANDLE =
            new PhoneAccountHandle(new ComponentName("pa_pkg", "pa_cls"), "pa_id");

@@ -121,9 +131,17 @@ public class InCallControllerTests extends TelecomTestCase {
        doReturn(true).when(mMockResources).getBoolean(R.bool.grant_location_permission_enabled);
        mEmergencyCallHelper = new EmergencyCallHelper(mMockContext, SYS_PKG,
                mTimeoutsAdapter);
        when(mMockCallsManager.getRoleManagerAdapter()).thenReturn(mMockRoleManagerAdapter);
        mInCallController = new InCallController(mMockContext, mLock, mMockCallsManager,
                mMockSystemStateHelper, mDefaultDialerCache, mTimeoutsAdapter,
                mEmergencyCallHelper);
        // Companion Apps don't have CONTROL_INCALL_EXPERIENCE permission.
        when(mMockPackageManager.checkPermission(
                matches(Manifest.permission.CONTROL_INCALL_EXPERIENCE),
                matches(COMPANION_PKG))).thenReturn(PackageManager.PERMISSION_DENIED);
        when(mMockPackageManager.checkPermission(
                matches(Manifest.permission.CONTROL_INCALL_EXPERIENCE),
                matches(CAR_PKG))).thenReturn(PackageManager.PERMISSION_DENIED);
    }

    @Override
@@ -493,6 +511,103 @@ public class InCallControllerTests extends TelecomTestCase {
        verify(mockInCallService).addCall(any(ParcelableCall.class));
    }

    /**
     * Ensures that the {@link InCallController} will bind to an {@link InCallService} which
     * supports third party companion calls.
     */
    @MediumTest
    public void doNotTestBindToService_Companion() throws Exception {
        setupMocks(true /* isExternalCall */);
        setupMockPackageManager(true /* default */, true /* system */, true /* external calls */);

        List<String> companionAppsList = new ArrayList<>();
        companionAppsList.add(COMPANION_PKG);
        companionAppsList.add(COMPANION_PKG);
        when(mMockRoleManagerAdapter.getCallCompanionApps()).thenReturn(companionAppsList);
        mInCallController.bindToServices(mMockCall);

        // Query for the different InCallServices
        ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
        verify(mMockPackageManager, times(6)).queryIntentServicesAsUser(
                queryIntentCaptor.capture(),
                eq(PackageManager.GET_META_DATA), eq(CURRENT_USER_ID));
        // Verify call for default dialer InCallService
        assertEquals(DEF_PKG, queryIntentCaptor.getAllValues().get(0).getPackage());
        // Verify call for system dialer InCallService
        assertEquals(null, queryIntentCaptor.getAllValues().get(1).getPackage());
        // Verify call for car mode ui InCallServices
        assertEquals(null, queryIntentCaptor.getAllValues().get(2).getPackage());
        // Verify call for non-UI InCallServices
        assertEquals(null, queryIntentCaptor.getAllValues().get(3).getPackage());
        // Verify call for companion InCallServices
        assertEquals(COMPANION_PKG, queryIntentCaptor.getAllValues().get(4).getPackage());
        assertEquals(COMPANION_PKG, queryIntentCaptor.getAllValues().get(5).getPackage());

        // Bind InCallServices
        ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
        verify(mMockContext, times(3)).bindServiceAsUser(
                bindIntentCaptor.capture(),
                any(ServiceConnection.class),
                eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE),
                eq(UserHandle.CURRENT));
        // Verify bind dialer
        Intent bindIntent = bindIntentCaptor.getAllValues().get(0);
        assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
        assertEquals(DEF_PKG, bindIntent.getComponent().getPackageName());
        assertEquals(DEF_CLASS, bindIntent.getComponent().getClassName());
        // Verify bind companion apps
        bindIntent = bindIntentCaptor.getAllValues().get(1);
        assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
        assertEquals(COMPANION_PKG, bindIntent.getComponent().getPackageName());
        assertEquals(COMPANION_CLASS, bindIntent.getComponent().getClassName());
        bindIntent = bindIntentCaptor.getAllValues().get(2);
        assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
        assertEquals(COMPANION_PKG, bindIntent.getComponent().getPackageName());
        assertEquals(COMPANION_CLASS, bindIntent.getComponent().getClassName());
    }

    /**
     * Ensures that the {@link InCallController} will bind to an {@link InCallService} which
     * supports third party car mode ui calls
     */
    @MediumTest
    public void doNotTestBindToService_CarModeUI() throws Exception {
        setupMocks(true /* isExternalCall */);
        setupMockPackageManager(true /* default */, true /* system */, true /* external calls */);

        when(mMockRoleManagerAdapter.getCarModeDialerApp()).thenReturn(CAR_PKG);
        // Enable car mode
        when(mMockSystemStateHelper.isCarMode()).thenReturn(true);
        mInCallController.bindToServices(mMockCall);

        // Query for the different InCallServices
        ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
        verify(mMockPackageManager, times(4)).queryIntentServicesAsUser(
                queryIntentCaptor.capture(),
                eq(PackageManager.GET_META_DATA), eq(CURRENT_USER_ID));
        // Verify call for default dialer InCallService
        assertEquals(DEF_PKG, queryIntentCaptor.getAllValues().get(0).getPackage());
        // Verify call for system dialer InCallService
        assertEquals(null, queryIntentCaptor.getAllValues().get(1).getPackage());
        // Verify call for car mode ui InCallServices
        assertEquals(CAR_PKG, queryIntentCaptor.getAllValues().get(2).getPackage());
        // Verify call for non-UI InCallServices
        assertEquals(null, queryIntentCaptor.getAllValues().get(3).getPackage());

        // Bind InCallServices
        ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
        verify(mMockContext, times(1)).bindServiceAsUser(
                bindIntentCaptor.capture(),
                any(ServiceConnection.class),
                eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE),
                eq(UserHandle.CURRENT));
        // Verify bind car mode ui
        Intent bindIntent = bindIntentCaptor.getAllValues().get(0);
        assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
        assertEquals(CAR_PKG, bindIntent.getComponent().getPackageName());
        assertEquals(CAR_CLASS, bindIntent.getComponent().getClassName());
    }

    private void setupMocks(boolean isExternalCall) {
        when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle);
        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
@@ -521,6 +636,22 @@ public class InCallControllerTests extends TelecomTestCase {
        }};
    }

    private ResolveInfo getCarModeResolveinfo(final boolean includeExternalCalls) {
        return new ResolveInfo() {{
            serviceInfo = new ServiceInfo();
            serviceInfo.packageName = CAR_PKG;
            serviceInfo.name = CAR_CLASS;
            serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
            serviceInfo.metaData = new Bundle();
            serviceInfo.metaData.putBoolean(
                    TelecomManager.METADATA_IN_CALL_SERVICE_CAR_MODE_UI, true);
            if (includeExternalCalls) {
                serviceInfo.metaData.putBoolean(
                        TelecomManager.METADATA_INCLUDE_EXTERNAL_CALLS, true);
            }
        }};
    }

    private ResolveInfo getSysResolveinfo() {
        return new ResolveInfo() {{
            serviceInfo = new ServiceInfo();
@@ -530,6 +661,15 @@ public class InCallControllerTests extends TelecomTestCase {
        }};
    }

    private ResolveInfo getCompanionResolveinfo() {
        return new ResolveInfo() {{
            serviceInfo = new ServiceInfo();
            serviceInfo.packageName = COMPANION_PKG;
            serviceInfo.name = COMPANION_CLASS;
            serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
        }};
    }

    private void setupMockPackageManager(final boolean useDefaultDialer,
            final boolean useSystemDialer, final boolean includeExternalCalls) {

@@ -545,15 +685,21 @@ public class InCallControllerTests extends TelecomTestCase {
                }
                LinkedList<ResolveInfo> resolveInfo = new LinkedList<ResolveInfo>();
                if (!TextUtils.isEmpty(packageName)) {
                    if ((TextUtils.isEmpty(packageName) || packageName.equals(DEF_PKG)) &&
                            useDefaultDialer) {
                    if (packageName.equals(DEF_PKG) && useDefaultDialer) {
                        resolveInfo.add(getDefResolveInfo(includeExternalCalls));
                    }

                    if ((TextUtils.isEmpty(packageName) || packageName.equals(SYS_PKG)) &&
                           useSystemDialer) {
                    if (packageName.equals(SYS_PKG) && useSystemDialer) {
                        resolveInfo.add(getSysResolveinfo());
                    }

                    if (packageName.equals(COMPANION_PKG)) {
                        resolveInfo.add(getCompanionResolveinfo());
                    }

                    if (packageName.equals(CAR_PKG)) {
                        resolveInfo.add(getCarModeResolveinfo(includeExternalCalls));
                    }
                }
                return resolveInfo;
            }