diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java index 2bebba991..4beff9f11 100644 --- a/src/com/android/server/telecom/InCallController.java +++ b/src/com/android/server/telecom/InCallController.java @@ -28,6 +28,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.PermissionChecker; import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; @@ -1524,7 +1525,7 @@ public class InCallController extends CallsManagerListenerBase { if (requestedType == IN_CALL_SERVICE_TYPE_NON_UI) { mKnownNonUiInCallServices.add(foundComponentName); } - + boolean isEnabled = isServiceEnabled(foundComponentName, serviceInfo, packageManager); if (isEnabled && (requestedType == 0 || requestedType == currentType)) { @@ -1594,9 +1595,17 @@ public class InCallController extends CallsManagerListenerBase { p -> packageManager.checkPermission( Manifest.permission.CONTROL_INCALL_EXPERIENCE, p) == PackageManager.PERMISSION_GRANTED); + + boolean hasAppOpsPermittedManageOngoingCalls = false; + if (isAppOpsPermittedManageOngoingCalls(serviceInfo.applicationInfo.uid, + serviceInfo.packageName)) { + hasAppOpsPermittedManageOngoingCalls = true; + } + boolean isCarModeUIService = serviceInfo.metaData != null && serviceInfo.metaData.getBoolean( TelecomManager.METADATA_IN_CALL_SERVICE_CAR_MODE_UI, false); + if (isCarModeUIService && hasControlInCallPermission) { return IN_CALL_SERVICE_TYPE_CAR_MODE_UI; } @@ -1611,7 +1620,8 @@ 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 (!isUIService && !isCarModeUIService && hasControlInCallPermission) { + if (!isUIService && !isCarModeUIService && (hasControlInCallPermission || + hasAppOpsPermittedManageOngoingCalls)) { return IN_CALL_SERVICE_TYPE_NON_UI; } @@ -2028,6 +2038,12 @@ public class InCallController extends CallsManagerListenerBase { return mCallsManager.getAudioState().isMuted(); } + private boolean isAppOpsPermittedManageOngoingCalls(int uid, String callingPackage) { + return PermissionChecker.checkPermissionForPreflight(mContext, + Manifest.permission.MANAGE_ONGOING_CALLS, PermissionChecker.PID_UNKNOWN, uid, + callingPackage) == PermissionChecker.PERMISSION_GRANTED; + } + private void sendCrashedInCallServiceNotification(String packageName) { PackageManager packageManager = mContext.getPackageManager(); CharSequence appName; diff --git a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java index af062d77f..a4302b6bf 100644 --- a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java +++ b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java @@ -42,6 +42,7 @@ import android.content.ServiceConnection; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.content.pm.PermissionInfo; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.res.Configuration; @@ -485,6 +486,7 @@ public class ComponentContextFixture implements TestFixture { private final RoleManager mRoleManager = mock(RoleManager.class); private final TelephonyRegistryManager mTelephonyRegistryManager = mock(TelephonyRegistryManager.class); + private final PermissionInfo mPermissionInfo = mock(PermissionInfo.class); private TelecomManager mTelecomManager = mock(TelecomManager.class); @@ -539,6 +541,14 @@ public class ComponentContextFixture implements TestFixture { matches(Manifest.permission.CALL_COMPANION_APP), anyString())) .thenReturn(PackageManager.PERMISSION_DENIED); + try { + when(mPackageManager.getPermissionInfo(anyString(), anyInt())).thenReturn( + mPermissionInfo); + } catch (PackageManager.NameNotFoundException ex) { + } + + when(mPermissionInfo.isAppOp()).thenReturn(true); + // Used in CreateConnectionProcessor to rank emergency numbers by viability. // For the test, make them all equal to INVALID so that the preferred PhoneAccount will be // chosen. diff --git a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java index 826a197d7..59f75fc20 100644 --- a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java +++ b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java @@ -52,6 +52,7 @@ import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.content.pm.PermissionInfo; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.res.Resources; @@ -122,6 +123,7 @@ public class InCallControllerTests extends TelecomTestCase { @Mock ClockProxy mClockProxy; @Mock Analytics.CallInfoImpl mCallInfo; @Mock NotificationManager mNotificationManager; + @Mock PermissionInfo mMockPermissionInfo; private static final int CURRENT_USER_ID = 900973; private static final String DEF_PKG = "defpkg"; @@ -142,6 +144,9 @@ public class InCallControllerTests extends TelecomTestCase { private static final String NONUI_PKG = "nonui_pkg"; private static final String NONUI_CLASS = "nonui_cls"; private static final int NONUI_UID = 6; + private static final String APPOP_NONUI_PKG = "appop_nonui_pkg"; + private static final String APPOP_NONUI_CLASS = "appop_nonui_cls"; + private static final int APPOP_NONUI_UID = 7; private static final PhoneAccountHandle PA_HANDLE = new PhoneAccountHandle(new ComponentName("pa_pkg", "pa_cls"), "pa_id"); @@ -173,6 +178,8 @@ public class InCallControllerTests extends TelecomTestCase { when(mMockCallsManager.getRoleManagerAdapter()).thenReturn(mMockRoleManagerAdapter); when(mMockContext.getSystemService(eq(Context.NOTIFICATION_SERVICE))) .thenReturn(mNotificationManager); + when(mMockPackageManager.getPermissionInfo(anyString(), anyInt())).thenReturn( + mMockPermissionInfo); mInCallController = new InCallController(mMockContext, mLock, mMockCallsManager, mMockSystemStateHelper, mDefaultDialerCache, mTimeoutsAdapter, mEmergencyCallHelper, mCarModeTracker, mClockProxy); @@ -198,6 +205,8 @@ public class InCallControllerTests extends TelecomTestCase { return new String[] { CAR2_PKG }; case NONUI_UID: return new String[] { NONUI_PKG }; + case APPOP_NONUI_UID: + return new String[] { APPOP_NONUI_PKG }; } return null; }).when(mMockPackageManager).getPackagesForUid(anyInt()); @@ -213,6 +222,9 @@ public class InCallControllerTests extends TelecomTestCase { when(mMockPackageManager.checkPermission( matches(Manifest.permission.CONTROL_INCALL_EXPERIENCE), matches(NONUI_PKG))).thenReturn(PackageManager.PERMISSION_GRANTED); + when(mMockPackageManager.checkPermission( + matches(Manifest.permission.CONTROL_INCALL_EXPERIENCE), + matches(APPOP_NONUI_PKG))).thenReturn(PackageManager.PERMISSION_DENIED); when(mMockCallsManager.getAudioState()).thenReturn(new CallAudioState(false, 0, 0)); } @@ -822,6 +834,49 @@ public class InCallControllerTests extends TelecomTestCase { verifyBinding(bindIntentCaptor, 0, DEF_PKG, DEF_CLASS); } + /** + * Ensures that the {@link InCallController} will bind to an {@link InCallService} which + * supports third party app + */ + @MediumTest + @Test + public void testBindToService_ThirdPartyApp() throws Exception { + setupMocks(false /* isExternalCall */); + setupMockPackageManager(false /* default */, false /* nonui */, true /* appop_nonui */, + true /* system */, false /* external calls */, false /* self mgd in default */, + false /* self mgd in car*/); + + // Enable Third Party Companion App + when(mMockPackageManager.getPermissionInfo(anyString(), anyInt())).thenReturn( + mMockPermissionInfo); + when(mMockPermissionInfo.isAppOp()).thenReturn(true); + when(mMockAppOpsManager.unsafeCheckOpRawNoThrow(matches( + AppOpsManager.OPSTR_MANAGE_ONGOING_CALLS), eq(APPOP_NONUI_UID), + matches(APPOP_NONUI_PKG))).thenReturn(AppOpsManager.MODE_ALLOWED); + + // Now bind; we should bind to the system dialer and app op non ui app. + mInCallController.bindToServices(mMockCall); + + // Bind InCallServices + ArgumentCaptor bindIntentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(mMockContext, times(2)).bindServiceAsUser( + bindIntentCaptor.capture(), + any(ServiceConnection.class), + eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE + | Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS), + eq(UserHandle.CURRENT)); + + // Verify bind + assertEquals(2, bindIntentCaptor.getAllValues().size()); + + // Should have first bound to the system dialer. + verifyBinding(bindIntentCaptor, 0, SYS_PKG, SYS_CLASS); + + // Should have next bound to the third party app op non ui app. + verifyBinding(bindIntentCaptor, 1, APPOP_NONUI_PKG, APPOP_NONUI_CLASS); + } + + @MediumTest @Test public void testSanitizeContactName() throws Exception { @@ -934,8 +989,8 @@ public class InCallControllerTests extends TelecomTestCase { nullable(ContentResolver.class))).thenReturn(500L); when(mMockCallsManager.getCalls()).thenReturn(Collections.singletonList(mMockCall)); - setupMockPackageManager(true /* default */, true /* nonui */, true /* system */, - false /* external calls */, + setupMockPackageManager(true /* default */, true /* nonui */, false /* appop_nonui */ , + true /* system */, false /* external calls */, false /* self mgd in default*/, false /* self mgd in car*/); mInCallController.bindToServices(mMockCall); @@ -1195,9 +1250,21 @@ public class InCallControllerTests extends TelecomTestCase { }}; } + private ResolveInfo getAppOpNonUiResolveinfo() { + return new ResolveInfo() {{ + serviceInfo = new ServiceInfo(); + serviceInfo.packageName = APPOP_NONUI_PKG; + serviceInfo.name = APPOP_NONUI_CLASS; + serviceInfo.applicationInfo = new ApplicationInfo(); + serviceInfo.applicationInfo.uid = APPOP_NONUI_UID; + serviceInfo.enabled = true; + serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE; + }}; + } + private void setupMockPackageManager(final boolean useDefaultDialer, final boolean useSystemDialer, final boolean includeExternalCalls) { - setupMockPackageManager(useDefaultDialer, false, useSystemDialer, includeExternalCalls, + setupMockPackageManager(useDefaultDialer, false, false, useSystemDialer, includeExternalCalls, false /* self mgd */, false /* self mgd */); } @@ -1205,13 +1272,13 @@ public class InCallControllerTests extends TelecomTestCase { final boolean useSystemDialer, final boolean includeExternalCalls, final boolean includeSelfManagedCallsInDefaultDialer, final boolean includeSelfManagedCallsInCarModeDialer) { - setupMockPackageManager(useDefaultDialer, false /* nonui */, useSystemDialer, - includeExternalCalls, includeSelfManagedCallsInDefaultDialer, + setupMockPackageManager(useDefaultDialer, false /* nonui */, false /* appop_nonui */, + useSystemDialer, includeExternalCalls, includeSelfManagedCallsInDefaultDialer, includeSelfManagedCallsInCarModeDialer); } private void setupMockPackageManager(final boolean useDefaultDialer, - final boolean useNonUiInCalls, + final boolean useNonUiInCalls, final boolean useAppOpNonUiInCalls, final boolean useSystemDialer, final boolean includeExternalCalls, final boolean includeSelfManagedCallsInDefaultDialer, final boolean includeSelfManagedCallsInCarModeDialer) { @@ -1254,6 +1321,10 @@ public class InCallControllerTests extends TelecomTestCase { if (useNonUiInCalls) { resolveInfo.add(getNonUiResolveinfo()); } + // InCallController uses a blank package name when querying for App Op non-ui incalls + if (useAppOpNonUiInCalls) { + resolveInfo.add(getAppOpNonUiResolveinfo()); + } } return resolveInfo;