• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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