1 /* 2 * Copyright (C) 2025 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.telecom.cts; 18 19 import static android.telecom.cts.TestUtils.waitOnAllHandlers; 20 21 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; 22 23 import android.Manifest; 24 import android.app.role.RoleManager; 25 import android.content.ComponentName; 26 import android.content.ContentResolver; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.ServiceConnection; 30 import android.content.pm.PackageManager; 31 import android.database.Cursor; 32 import android.net.Uri; 33 import android.os.Bundle; 34 import android.os.IBinder; 35 import android.os.Process; 36 import android.os.RemoteException; 37 import android.os.UserHandle; 38 import android.provider.CallLog; 39 import android.telecom.Call; 40 import android.telecom.TelecomManager; 41 import android.telecom.cts.screeningtestapp.CallScreeningServiceControl; 42 import android.telecom.cts.screeningtestapp.CtsCallScreeningService; 43 import android.telecom.cts.screeningtestapp.ICallScreeningControl; 44 import android.text.TextUtils; 45 import android.util.Log; 46 47 import java.util.List; 48 import java.util.concurrent.CountDownLatch; 49 import java.util.concurrent.Executor; 50 import java.util.concurrent.LinkedBlockingQueue; 51 import java.util.concurrent.TimeUnit; 52 53 /** 54 * Abstract base class for tests of the third-party app {@link android.telecom.CallScreeningService} 55 * . This class handles the common setup, teardown, and helper methods required for testing call 56 * screening interactions with the Telecom framework. 57 * 58 * <p>Subclasses should implement specific test cases related to different aspects of call 59 * screening, such as permission handling, call rejection, call silencing, and interaction with the 60 * call log. 61 * 62 * <p>This base class manages: 63 * 64 * <ul> 65 * <li>Binding to and controlling the test {@link android.telecom.CallScreeningService} (located 66 * in a separate APK). 67 * <li>Granting and revoking the {@link android.Manifest.permission#READ_CONTACTS} permission to 68 * the test app. 69 * <li>Managing the {@link android.app.role.RoleManager#ROLE_CALL_SCREENING} role, ensuring the 70 * test app holds the role during tests and restoring the previous role holder afterward. 71 * <li>Providing helper methods for simulating incoming and outgoing calls, and for verifying call 72 * log entries. 73 * <li>Cleaning up resources (unbinding from the service, deleting contacts) after tests. 74 * </ul> 75 * 76 * <p>Subclasses can use the protected methods provided by this class to interact with the test app, 77 * simulate calls, check permissions, verify call log data and access the useful tools. 78 */ 79 public abstract class BaseThirdPartyCallScreeningServiceTest 80 extends BaseTelecomTestWithMockServices { 81 public static final String EXTRA_NETWORK_IDENTIFIED_EMERGENCY_CALL = "identifiedEmergencyCall"; 82 private static final String TAG = BaseThirdPartyCallScreeningServiceTest.class.getSimpleName(); 83 protected static final String TEST_APP_NAME = "CTSCSTest"; 84 protected static final String TEST_APP_PACKAGE = "android.telecom.cts.screeningtestapp"; 85 protected static final String TEST_APP_COMPONENT = 86 "android.telecom.cts.screeningtestapp/" 87 + "android.telecom.cts.screeningtestapp.CtsCallScreeningService"; 88 protected static final int ASYNC_TIMEOUT = 10000; 89 public static final String ROLE_CALL_SCREENING = RoleManager.ROLE_CALL_SCREENING; 90 protected static final Uri TEST_OUTGOING_NUMBER = Uri.fromParts("tel", "6505551212", null); 91 92 protected ICallScreeningControl mCallScreeningControl; 93 protected RoleManager mRoleManager; 94 private String mPreviousCallScreeningPackage; 95 protected PackageManager mPackageManager; 96 protected Uri mContactUri; 97 protected ContentResolver mContentResolver; 98 protected ServiceConnection mServiceConnection; // Make ServiceConnection accessible 99 100 @Override setUp()101 protected void setUp() throws Exception { 102 super.setUp(); 103 if (!mShouldTestTelecom) { 104 return; 105 } 106 mRoleManager = mContext.getSystemService(RoleManager.class); 107 mPackageManager = mContext.getPackageManager(); 108 mContentResolver = getInstrumentation().getTargetContext().getContentResolver(); 109 rememberPreviousCallScreeningApp(); 110 } 111 112 @Override tearDown()113 protected void tearDown() throws Exception { 114 if (mShouldTestTelecom) { 115 resetAndRestoreCallScreening(); 116 } 117 super.tearDown(); 118 } 119 setupCallScreening()120 protected void setupCallScreening() throws Exception { 121 mServiceConnection = bindToTestApp(); 122 setupConnectionService(null, FLAG_REGISTER | FLAG_ENABLE); 123 // Ensure CTS app holds the call screening role. 124 addRoleHolder(ROLE_CALL_SCREENING, CtsCallScreeningService.class.getPackage().getName()); 125 } 126 bindToTestApp()127 protected ServiceConnection bindToTestApp() throws Exception { 128 Intent bindIntent = new Intent(CallScreeningServiceControl.CONTROL_INTERFACE_ACTION); 129 bindIntent.setComponent(CallScreeningServiceControl.CONTROL_INTERFACE_COMPONENT); 130 final CountDownLatch bindLatch = new CountDownLatch(1); 131 132 ServiceConnection serviceConnection = 133 new ServiceConnection() { 134 @Override 135 public void onServiceConnected(ComponentName name, IBinder service) { 136 Log.i(TAG, "onServiceConnected: " + name); 137 mCallScreeningControl = ICallScreeningControl.Stub.asInterface(service); 138 bindLatch.countDown(); 139 } 140 141 @Override 142 public void onServiceDisconnected(ComponentName name) { 143 // The onServiceDisconnected() callback is not invoked when you explicitly 144 // unbind; it's only called when the connection is unexpectedly lost. 145 Log.i(TAG, "onServiceDisconnected: " + name); 146 mCallScreeningControl = null; 147 } 148 }; 149 150 boolean success = 151 mContext.bindService( 152 bindIntent, 153 serviceConnection, 154 Context.BIND_AUTO_CREATE | Context.BIND_ABOVE_CLIENT); 155 if (!success) { 156 fail("Failed to get control interface -- bind error"); 157 } 158 boolean completedBeforeTimeout = bindLatch.await(ASYNC_TIMEOUT, TimeUnit.MILLISECONDS); 159 assertTrue(completedBeforeTimeout); 160 waitForScreeningControl(); 161 return serviceConnection; 162 } 163 waitForAppUnbinding()164 protected void waitForAppUnbinding() throws RemoteException { 165 if (mServiceConnection == null 166 || mCallScreeningControl == null 167 || !mCallScreeningControl.isBound()) { 168 Log.w(TAG, "waitForAppUnbinding: skipping unbind because service is already unbound"); 169 return; 170 } 171 Log.i(TAG, "waitForAppUnbinding: requesting control unbind"); 172 try { 173 mContext.unbindService(mServiceConnection); 174 } catch (Exception e) { 175 Log.w(TAG, "waitForAppUnbinding: hit an exception when calling unbind. e=[" + e + "]"); 176 } 177 waitUntilConditionIsTrueOrTimeout( 178 new Condition() { 179 @Override 180 public Object expected() { 181 return true; 182 } 183 184 @Override 185 public Object actual() { 186 try { 187 return mCallScreeningControl == null 188 || !mCallScreeningControl.isBound(); 189 } catch (RemoteException re) { 190 Log.e(TAG, "RemoteException checking binding", re); 191 return true; 192 } 193 } 194 }, 195 TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS, 196 "mCallScreeningControl object which represents binding to the test app is NOT " 197 + "null. This means the app is still bound when it should be unbound."); 198 Log.i(TAG, "waitForAppUnbinding: done"); 199 } 200 resetAndRestoreCallScreening()201 protected void resetAndRestoreCallScreening() throws Exception { 202 if (mCallScreeningControl != null) { 203 mCallScreeningControl.reset(); 204 } 205 // Remove the test app from the screening role. 206 removeRoleHolder(ROLE_CALL_SCREENING, CtsCallScreeningService.class.getPackage().getName()); 207 208 if (!TextUtils.isEmpty(mPreviousCallScreeningPackage)) { 209 addRoleHolder(ROLE_CALL_SCREENING, mPreviousCallScreeningPackage); 210 } 211 waitForAppUnbinding(); 212 } 213 rememberPreviousCallScreeningApp()214 private void rememberPreviousCallScreeningApp() { 215 runWithShellPermissionIdentity( 216 () -> { 217 List<String> callScreeningApps = 218 mRoleManager.getRoleHolders(ROLE_CALL_SCREENING); 219 if (!callScreeningApps.isEmpty()) { 220 mPreviousCallScreeningPackage = callScreeningApps.get(0); 221 } else { 222 mPreviousCallScreeningPackage = null; 223 } 224 }); 225 } 226 addRoleHolder(String roleName, String packageName)227 protected void addRoleHolder(String roleName, String packageName) throws Exception { 228 UserHandle user = Process.myUserHandle(); 229 Executor executor = mContext.getMainExecutor(); 230 LinkedBlockingQueue<Boolean> queue = new LinkedBlockingQueue(1); 231 232 runWithShellPermissionIdentity( 233 () -> 234 mRoleManager.addRoleHolderAsUser( 235 roleName, 236 packageName, 237 RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP, 238 user, 239 executor, 240 successful -> { 241 try { 242 queue.put(successful); 243 } catch (InterruptedException e) { 244 Log.w( 245 TAG, 246 String.format( 247 "encountered InterruptedException" 248 + " e=[%s]", 249 e)); 250 } 251 })); 252 boolean result = queue.poll(ASYNC_TIMEOUT, TimeUnit.MILLISECONDS); 253 assertTrue(result); 254 } 255 removeRoleHolder(String roleName, String packageName)256 protected void removeRoleHolder(String roleName, String packageName) throws Exception { 257 UserHandle user = Process.myUserHandle(); 258 Executor executor = mContext.getMainExecutor(); 259 LinkedBlockingQueue<Boolean> queue = new LinkedBlockingQueue(1); 260 261 runWithShellPermissionIdentity( 262 () -> 263 mRoleManager.removeRoleHolderAsUser( 264 roleName, 265 packageName, 266 RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP, 267 user, 268 executor, 269 successful -> { 270 try { 271 queue.put(successful); 272 } catch (InterruptedException e) { 273 Log.w( 274 TAG, 275 String.format( 276 "encountered InterruptedException" 277 + " e=[%s]", 278 e)); 279 } 280 })); 281 boolean result = queue.poll(ASYNC_TIMEOUT, TimeUnit.MILLISECONDS); 282 assertTrue(result); 283 } 284 grantReadContactPermission()285 protected void grantReadContactPermission() { 286 runWithShellPermissionIdentity( 287 () -> { 288 if (mPackageManager != null) { 289 mPackageManager.grantRuntimePermission( 290 TEST_APP_PACKAGE, 291 Manifest.permission.READ_CONTACTS, 292 mContext.getUser()); 293 } 294 }); 295 } 296 revokeReadContactPermission()297 protected void revokeReadContactPermission() { 298 runWithShellPermissionIdentity( 299 () -> { 300 if (mPackageManager != null) { 301 mPackageManager.revokeRuntimePermission( 302 TEST_APP_PACKAGE, 303 Manifest.permission.READ_CONTACTS, 304 mContext.getUser()); 305 } 306 }); 307 } 308 verifyPermission(boolean hasPermission)309 protected void verifyPermission(boolean hasPermission) { 310 assertEquals( 311 hasPermission, 312 mPackageManager.checkPermission(Manifest.permission.READ_CONTACTS, TEST_APP_PACKAGE) 313 == PackageManager.PERMISSION_GRANTED); 314 } 315 placeOutgoingCall(boolean addContact)316 protected void placeOutgoingCall(boolean addContact) throws Exception { 317 // Setup content observer to notify us when we call log entry is added. 318 CountDownLatch callLogEntryLatch = getCallLogEntryLatch(); 319 320 Uri contactUri = null; 321 if (addContact) { 322 contactUri = 323 TestUtils.insertContact( 324 mContentResolver, TEST_OUTGOING_NUMBER.getSchemeSpecificPart()); 325 } 326 327 try { 328 Bundle extras = new Bundle(); 329 extras.putParcelable(TestUtils.EXTRA_PHONE_NUMBER, TEST_OUTGOING_NUMBER); 330 // Create a new outgoing call. 331 placeAndVerifyCall(extras); 332 333 disconnectAllCalls(); 334 335 // Wait for it to log. 336 boolean completedBeforeTimeout = 337 callLogEntryLatch.await(ASYNC_TIMEOUT, TimeUnit.MILLISECONDS); 338 assertTrue(completedBeforeTimeout); 339 } finally { 340 if (addContact) { 341 assertEquals(1, TestUtils.deleteContact(mContentResolver, contactUri)); 342 } 343 } 344 } 345 addIncoming( boolean disconnectImmediately, boolean addContact, boolean skipCallLogLatch)346 protected Uri addIncoming( 347 boolean disconnectImmediately, boolean addContact, boolean skipCallLogLatch) 348 throws Exception { 349 // Add call through TelecomManager; we can't use the test methods since they assume a call 350 // makes it through to the InCallService; this is blocked so it shouldn't. 351 Uri testNumber = createRandomTestNumber(); 352 if (addContact) { 353 mContactUri = 354 TestUtils.insertContact(mContentResolver, testNumber.getSchemeSpecificPart()); 355 } 356 357 // Setup content observer to notify us when we call log entry is added. 358 CountDownLatch callLogEntryLatch = null; 359 if (!skipCallLogLatch) { 360 callLogEntryLatch = getCallLogEntryLatch(); 361 } 362 363 Bundle extras = new Bundle(); 364 extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, testNumber); 365 mTelecomManager.addNewIncomingCall(TestUtils.TEST_PHONE_ACCOUNT_HANDLE, extras); 366 367 // Wait until the new incoming call is processed. 368 waitOnAllHandlers(getInstrumentation()); 369 370 if (disconnectImmediately) { 371 // Disconnect the call 372 disconnectAllCalls(); 373 } 374 375 // Wait for the content observer to report that we have gotten a new call log entry. 376 if (!skipCallLogLatch) { 377 boolean completedBeforeTimeout = 378 callLogEntryLatch.await(ASYNC_TIMEOUT, TimeUnit.MILLISECONDS); 379 assertTrue(completedBeforeTimeout); 380 } 381 return testNumber; 382 } 383 addIncomingAndVerifyAllowed(boolean addContact)384 protected void addIncomingAndVerifyAllowed(boolean addContact) throws Exception { 385 Uri testNumber = addIncoming(true, addContact, false); 386 387 // Query the latest entry into the call log. 388 Cursor callsCursor = 389 mContentResolver.query( 390 CallLog.Calls.CONTENT_URI, 391 null, 392 null, 393 null, 394 CallLog.Calls._ID + " DESC limit 1;"); 395 int numberIndex = callsCursor.getColumnIndex(CallLog.Calls.NUMBER); 396 int callTypeIndex = callsCursor.getColumnIndex(CallLog.Calls.TYPE); 397 int blockReasonIndex = callsCursor.getColumnIndex(CallLog.Calls.BLOCK_REASON); 398 if (callsCursor.moveToNext()) { 399 String number = callsCursor.getString(numberIndex); 400 int callType = callsCursor.getInt(callTypeIndex); 401 int blockReason = callsCursor.getInt(blockReasonIndex); 402 assertEquals(testNumber.getSchemeSpecificPart(), number); 403 assertEquals(CallLog.Calls.INCOMING_TYPE, callType); 404 assertEquals(CallLog.Calls.BLOCK_REASON_NOT_BLOCKED, blockReason); 405 } else { 406 fail("Call not logged"); 407 } 408 409 if (addContact && mContactUri != null) { 410 assertEquals(1, TestUtils.deleteContact(mContentResolver, mContactUri)); 411 } 412 } 413 addIncomingAndVerifyBlocked(boolean addContact)414 protected void addIncomingAndVerifyBlocked(boolean addContact) throws Exception { 415 Uri testNumber = addIncoming(false, addContact, false); 416 417 // Query the latest entry into the call log. 418 Cursor callsCursor = 419 mContentResolver.query( 420 CallLog.Calls.CONTENT_URI, 421 null, 422 null, 423 null, 424 CallLog.Calls._ID + " DESC limit 1;"); 425 int numberIndex = callsCursor.getColumnIndex(CallLog.Calls.NUMBER); 426 int callTypeIndex = callsCursor.getColumnIndex(CallLog.Calls.TYPE); 427 int blockReasonIndex = callsCursor.getColumnIndex(CallLog.Calls.BLOCK_REASON); 428 int callScreeningAppNameIndex = 429 callsCursor.getColumnIndex(CallLog.Calls.CALL_SCREENING_APP_NAME); 430 int callScreeningCmpNameIndex = 431 callsCursor.getColumnIndex(CallLog.Calls.CALL_SCREENING_COMPONENT_NAME); 432 if (callsCursor.moveToNext()) { 433 String number = callsCursor.getString(numberIndex); 434 int callType = callsCursor.getInt(callTypeIndex); 435 int blockReason = callsCursor.getInt(blockReasonIndex); 436 String screeningAppName = callsCursor.getString(callScreeningAppNameIndex); 437 String screeningComponentName = callsCursor.getString(callScreeningCmpNameIndex); 438 assertEquals(testNumber.getSchemeSpecificPart(), number); 439 assertEquals(CallLog.Calls.BLOCKED_TYPE, callType); 440 assertEquals(CallLog.Calls.BLOCK_REASON_CALL_SCREENING_SERVICE, blockReason); 441 assertEquals(TEST_APP_NAME, screeningAppName); 442 assertEquals(TEST_APP_COMPONENT, screeningComponentName); 443 } else { 444 fail("Blocked call was not logged."); 445 } 446 447 if (addContact && mContactUri != null) { 448 assertEquals(1, TestUtils.deleteContact(mContentResolver, mContactUri)); 449 } 450 } 451 addIncomingAndVerifyCallExtraForSilence(boolean expectedIsSilentRingingExtraSet)452 protected void addIncomingAndVerifyCallExtraForSilence(boolean expectedIsSilentRingingExtraSet) 453 throws Exception { 454 CountDownLatch callLogEntryLatch = getCallLogEntryLatch(); 455 addIncoming(false, false, true); 456 457 waitUntilConditionIsTrueOrTimeout( 458 new Condition() { 459 @Override 460 public Object expected() { 461 return true; 462 } 463 464 @Override 465 public Object actual() { 466 // Verify that the call extra matches expectation 467 Call call = mInCallCallbacks.getService().getLastCall(); 468 return expectedIsSilentRingingExtraSet 469 == call.getDetails() 470 .getExtras() 471 .getBoolean(Call.EXTRA_SILENT_RINGING_REQUESTED); 472 } 473 }, 474 TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS, 475 "Call extra - verification failed, expected the extra " 476 + "EXTRA_SILENT_RINGING_REQUESTED to be set:" 477 + expectedIsSilentRingingExtraSet); 478 479 // Logging does not get registered until we do explicit disconnection 480 disconnectAllCalls(); 481 boolean completedBeforeTimeout = 482 callLogEntryLatch.await(ASYNC_TIMEOUT, TimeUnit.MILLISECONDS); 483 assertTrue(completedBeforeTimeout); 484 } 485 disconnectAllCalls()486 private void disconnectAllCalls() { 487 if (mInCallCallbacks != null && mInCallCallbacks.getService() != null) { 488 mInCallCallbacks.getService().disconnectAllCalls(); 489 assertNumCalls(mInCallCallbacks.getService(), 0); 490 } 491 } 492 493 /** 494 * This helper waits for the call screening process to bind and call onServiceConnected. If the 495 * onServiceConnected is never called or called too early, an NPE can be hit while running the 496 * test. 497 */ waitForScreeningControl()498 protected void waitForScreeningControl() { 499 waitUntilConditionIsTrueOrTimeout( 500 new Condition() { 501 @Override 502 public Object expected() { 503 return true; 504 } 505 506 @Override 507 public Object actual() { 508 return mCallScreeningControl != null; 509 } 510 }, 511 5000, 512 "mCallScreeningControl is null which means onServiceConnected was never" + 513 "called"); 514 } 515 } 516