Merge "Binds to 3rd-party InCallService with MANAGE_ONGOING_CALL permission" into rvc-qpr-dev-plus-aosp
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index 9a1c876..954aa44 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -28,6 +28,7 @@
 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;
@@ -1576,9 +1577,17 @@
                 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;
         }
@@ -1593,7 +1602,8 @@
 
         // 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;
         }
 
@@ -2010,6 +2020,12 @@
         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 af062d7..a4302b6 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.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 @@
     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 @@
                 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 693859b..6a6b9f3 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.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 @@
     @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 @@
     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 @@
         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 @@
                     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 @@
         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 @@
         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<Intent> 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 @@
                 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 @@
         }};
     }
 
+    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 @@
             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 @@
                     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;