• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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 com.android.ecm;
18 
19 import static android.app.ecm.EnhancedConfirmationManager.REASON_PACKAGE_RESTRICTED;
20 import static android.app.ecm.EnhancedConfirmationManager.REASON_PHONE_STATE;
21 
22 import static com.android.permission.PermissionStatsLog.CALL_WITH_ECM_INTERACTION_REPORTED;
23 import static com.android.permissioncontroller.PermissionControllerStatsLog.ECM_RESTRICTION_QUERY_IN_CALL_REPORTED;
24 
25 import android.Manifest;
26 import android.accessibilityservice.AccessibilityServiceInfo;
27 import android.annotation.FlaggedApi;
28 import android.annotation.IntDef;
29 import android.annotation.NonNull;
30 import android.annotation.Nullable;
31 import android.annotation.SuppressLint;
32 import android.annotation.UserIdInt;
33 import android.annotation.WorkerThread;
34 import android.app.AppOpsManager;
35 import android.app.ecm.EnhancedConfirmationManager;
36 import android.app.ecm.IEnhancedConfirmationManager;
37 import android.app.role.RoleManager;
38 import android.content.ContentResolver;
39 import android.content.Context;
40 import android.content.pm.ApplicationInfo;
41 import android.content.pm.InstallSourceInfo;
42 import android.content.pm.PackageInstaller;
43 import android.content.pm.PackageManager;
44 import android.content.pm.PackageManager.NameNotFoundException;
45 import android.content.pm.SignedPackage;
46 import android.database.Cursor;
47 import android.net.Uri;
48 import android.os.Binder;
49 import android.os.Build;
50 import android.os.Looper;
51 import android.os.SystemClock;
52 import android.os.SystemConfigManager;
53 import android.os.UserHandle;
54 import android.permission.flags.Flags;
55 import android.provider.ContactsContract;
56 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
57 import android.provider.ContactsContract.PhoneLookup;
58 import android.telecom.Call;
59 import android.telecom.PhoneAccount;
60 import android.telephony.TelephonyManager;
61 import android.util.ArrayMap;
62 import android.util.ArraySet;
63 import android.util.Log;
64 import android.view.accessibility.AccessibilityManager;
65 
66 import androidx.annotation.Keep;
67 import androidx.annotation.RequiresApi;
68 
69 import com.android.internal.annotations.GuardedBy;
70 import com.android.internal.util.Preconditions;
71 import com.android.permission.util.UserUtils;
72 import com.android.permissioncontroller.PermissionControllerStatsLog;
73 import com.android.server.LocalManagerRegistry;
74 import com.android.server.SystemService;
75 
76 import java.lang.annotation.Retention;
77 import java.lang.annotation.RetentionPolicy;
78 import java.util.ArrayList;
79 import java.util.List;
80 import java.util.Map;
81 import java.util.Objects;
82 import java.util.Set;
83 import java.util.concurrent.ConcurrentHashMap;
84 import java.util.concurrent.TimeUnit;
85 
86 
87 /**
88  * Service for ECM (Enhanced Confirmation Mode).
89  *
90  * @see EnhancedConfirmationManager
91  *
92  * @hide
93  */
94 @Keep
95 @FlaggedApi(Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
96 @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
97 @SuppressLint("MissingPermission")
98 public class EnhancedConfirmationService extends SystemService {
99     private static final String LOG_TAG = EnhancedConfirmationService.class.getSimpleName();
100 
101     /** A map of ECM states to their corresponding app op states */
102     @Retention(java.lang.annotation.RetentionPolicy.SOURCE)
103     @IntDef(prefix = {"ECM_STATE_"}, value = {EcmState.ECM_STATE_NOT_GUARDED,
104             EcmState.ECM_STATE_GUARDED, EcmState.ECM_STATE_GUARDED_AND_ACKNOWLEDGED,
105             EcmState.ECM_STATE_IMPLICIT})
106     private @interface EcmState {
107         int ECM_STATE_NOT_GUARDED = AppOpsManager.MODE_ALLOWED;
108         int ECM_STATE_GUARDED = AppOpsManager.MODE_ERRORED;
109         int ECM_STATE_GUARDED_AND_ACKNOWLEDGED = AppOpsManager.MODE_IGNORED;
110         int ECM_STATE_IMPLICIT = AppOpsManager.MODE_DEFAULT;
111     }
112 
113     private static final ArraySet<String> PER_PACKAGE_PROTECTED_SETTINGS = new ArraySet<>();
114 
115     // Settings restricted when an untrusted call is ongoing. These must also be added to
116     // PROTECTED_SETTINGS
117     private static final ArraySet<String> UNTRUSTED_CALL_RESTRICTED_SETTINGS = new ArraySet<>();
118 
119     static {
120         // Runtime permissions
121         PER_PACKAGE_PROTECTED_SETTINGS.add(Manifest.permission.SEND_SMS);
122         PER_PACKAGE_PROTECTED_SETTINGS.add(Manifest.permission.RECEIVE_SMS);
123         PER_PACKAGE_PROTECTED_SETTINGS.add(Manifest.permission.READ_SMS);
124         PER_PACKAGE_PROTECTED_SETTINGS.add(Manifest.permission.RECEIVE_MMS);
125         PER_PACKAGE_PROTECTED_SETTINGS.add(Manifest.permission.RECEIVE_WAP_PUSH);
126         PER_PACKAGE_PROTECTED_SETTINGS.add(Manifest.permission.READ_CELL_BROADCASTS);
127         PER_PACKAGE_PROTECTED_SETTINGS.add(Manifest.permission_group.SMS);
128 
129         PER_PACKAGE_PROTECTED_SETTINGS.add(Manifest.permission.BIND_DEVICE_ADMIN);
130         // App ops
131         PER_PACKAGE_PROTECTED_SETTINGS.add(AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE);
132         PER_PACKAGE_PROTECTED_SETTINGS.add(AppOpsManager.OPSTR_ACCESS_NOTIFICATIONS);
133         PER_PACKAGE_PROTECTED_SETTINGS.add(AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW);
134         PER_PACKAGE_PROTECTED_SETTINGS.add(AppOpsManager.OPSTR_GET_USAGE_STATS);
135         PER_PACKAGE_PROTECTED_SETTINGS.add(AppOpsManager.OPSTR_LOADER_USAGE_STATS);
136         // Default application roles.
137         PER_PACKAGE_PROTECTED_SETTINGS.add(RoleManager.ROLE_DIALER);
138         PER_PACKAGE_PROTECTED_SETTINGS.add(RoleManager.ROLE_SMS);
139 
140         if (Flags.unknownCallPackageInstallBlockingEnabled()) {
141             // Requesting package installs, limited during phone calls
142             UNTRUSTED_CALL_RESTRICTED_SETTINGS.add(
143                     AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES);
144             UNTRUSTED_CALL_RESTRICTED_SETTINGS.add(
145                     AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE);
146         }
147     }
148 
149     private Map<String, List<byte[]>> mTrustedPackageCertDigests;
150     private Map<String, List<byte[]>> mTrustedInstallerCertDigests;
151     private static final int CALL_TYPE_UNTRUSTED = 0;
152     private static final int CALL_TYPE_TRUSTED = 1;
153     private static final int CALL_TYPE_EMERGENCY = 1 << 1;
154     @IntDef(flag = true, value = {
155             CALL_TYPE_UNTRUSTED,
156             CALL_TYPE_TRUSTED,
157             CALL_TYPE_EMERGENCY
158     })
159     @Retention(RetentionPolicy.SOURCE)
160     @interface CallType {}
161 
EnhancedConfirmationService(@onNull Context context)162     public EnhancedConfirmationService(@NonNull Context context) {
163         super(context);
164         LocalManagerRegistry.addManager(EnhancedConfirmationManagerLocal.class,
165                 new EnhancedConfirmationManagerLocalImpl(this));
166     }
167 
168     private PackageManager mPackageManager;
169 
170     // A helper which tracks the calls received by the system, and information about them.
171     private CallTracker mCallTracker;
172 
173     @GuardedBy("mUserAccessibilityManagers")
174     private final Map<Integer, AccessibilityManager> mUserAccessibilityManagers =
175             new ArrayMap<>();
176 
177     @Override
onStart()178     public void onStart() {
179         Context context = getContext();
180         SystemConfigManager systemConfigManager = context.getSystemService(
181                 SystemConfigManager.class);
182         mTrustedPackageCertDigests = toTrustedPackageMap(
183                 systemConfigManager.getEnhancedConfirmationTrustedPackages());
184         mTrustedInstallerCertDigests = toTrustedPackageMap(
185                 systemConfigManager.getEnhancedConfirmationTrustedInstallers());
186 
187         publishBinderService(Context.ECM_ENHANCED_CONFIRMATION_SERVICE, new Stub());
188 
189         if (Flags.unknownCallPackageInstallBlockingEnabled()) {
190             mCallTracker = new CallTracker(getContext());
191         }
192         mPackageManager = getContext().getPackageManager();
193     }
194 
toTrustedPackageMap(Set<SignedPackage> signedPackages)195     private Map<String, List<byte[]>> toTrustedPackageMap(Set<SignedPackage> signedPackages) {
196         ArrayMap<String, List<byte[]>> trustedPackageMap = new ArrayMap<>();
197         for (SignedPackage signedPackage : signedPackages) {
198             ArrayList<byte[]> certDigests = (ArrayList<byte[]>) trustedPackageMap.computeIfAbsent(
199                     signedPackage.getPackageName(), packageName -> new ArrayList<>(1));
200             certDigests.add(signedPackage.getCertificateDigest());
201         }
202         return trustedPackageMap;
203     }
204 
addOngoingCall(Call call)205     void addOngoingCall(Call call) {
206         assertNotMainThread();
207         if (mCallTracker != null) {
208             mCallTracker.addCall(call);
209         }
210     }
211 
212     @WorkerThread
removeOngoingCall(String callId)213     void removeOngoingCall(String callId) {
214         assertNotMainThread();
215         if (mCallTracker != null) {
216             mCallTracker.endCall(callId);
217         }
218     }
219 
220     @WorkerThread
clearOngoingCalls()221     void clearOngoingCalls() {
222         assertNotMainThread();
223 
224         if (mCallTracker != null) {
225             mCallTracker.endAllCalls();
226         }
227     }
228 
getPackageUid(PackageManager pm, String packageName, int userId)229     static int getPackageUid(PackageManager pm, String packageName,
230             int userId) throws NameNotFoundException {
231         return pm.getPackageUidAsUser(packageName, PackageManager.PackageInfoFlags.of(0), userId);
232     }
233 
assertNotMainThread()234     private void assertNotMainThread() throws IllegalStateException {
235         if (Looper.myLooper() == Looper.getMainLooper()) {
236             throw new IllegalStateException("Ecm WorkerThread method called on main thread");
237         }
238     }
239 
240     private class Stub extends IEnhancedConfirmationManager.Stub {
241 
242         private final @NonNull Context mContext;
243         private final String mAttributionTag;
244         private final AppOpsManager mAppOpsManager;
245 
Stub()246         Stub() {
247             Context context = getContext();
248             mContext = context;
249             mAttributionTag = context.getAttributionTag();
250             mAppOpsManager = context.getSystemService(AppOpsManager.class);
251         }
252 
isRestricted(@onNull String packageName, @NonNull String settingIdentifier, @UserIdInt int userId)253         public boolean isRestricted(@NonNull String packageName, @NonNull String settingIdentifier,
254                 @UserIdInt int userId) {
255             return getRestrictionReason(packageName, settingIdentifier, userId) != null;
256         }
257 
getRestrictionReason(@onNull String packageName, @NonNull String settingIdentifier, @UserIdInt int userId)258         public String getRestrictionReason(@NonNull String packageName,
259                 @NonNull String settingIdentifier,
260                 @UserIdInt int userId) {
261             enforcePermissions("isRestricted", userId);
262             if (!UserUtils.isUserExistent(userId, getContext())) {
263                 Log.e(LOG_TAG, "user " + userId + " does not exist");
264                 return null;
265             }
266 
267             Preconditions.checkStringNotEmpty(packageName, "packageName cannot be null or empty");
268             Preconditions.checkStringNotEmpty(settingIdentifier,
269                     "settingIdentifier cannot be null or empty");
270 
271             try {
272                 if (!isSettingEcmProtected(settingIdentifier)) {
273                     return null;
274                 }
275                 if (isSettingEcmGuardedForPackage(settingIdentifier, packageName, userId)) {
276                     return REASON_PACKAGE_RESTRICTED;
277                 }
278                 String globalProtectionReason =
279                         getGlobalProtectionReason(settingIdentifier, packageName, userId);
280                 if (globalProtectionReason != null) {
281                     return globalProtectionReason;
282                 }
283                 return null;
284             } catch (NameNotFoundException e) {
285                 throw new IllegalArgumentException(e);
286             }
287         }
288 
clearRestriction(@onNull String packageName, @UserIdInt int userId)289         public void clearRestriction(@NonNull String packageName, @UserIdInt int userId) {
290             enforcePermissions("clearRestriction", userId);
291             if (!UserUtils.isUserExistent(userId, getContext())) {
292                 return;
293             }
294 
295             Preconditions.checkStringNotEmpty(packageName, "packageName cannot be null or empty");
296 
297             try {
298                 int state = getAppEcmState(packageName, userId);
299                 boolean isAllowed = state == EcmState.ECM_STATE_GUARDED_AND_ACKNOWLEDGED;
300                 if (!isAllowed) {
301                     throw new IllegalStateException("Clear restriction attempted but not allowed");
302                 }
303                 setAppEcmState(packageName, EcmState.ECM_STATE_NOT_GUARDED, userId);
304                 EnhancedConfirmationStatsLogUtils.INSTANCE.logRestrictionCleared(
305                         getPackageUid(mPackageManager, packageName, userId));
306             } catch (NameNotFoundException e) {
307                 throw new IllegalArgumentException(e);
308             }
309         }
310 
isClearRestrictionAllowed(@onNull String packageName, @UserIdInt int userId)311         public boolean isClearRestrictionAllowed(@NonNull String packageName,
312                 @UserIdInt int userId) {
313             enforcePermissions("isClearRestrictionAllowed", userId);
314             if (!UserUtils.isUserExistent(userId, getContext())) {
315                 return false;
316             }
317 
318             Preconditions.checkStringNotEmpty(packageName, "packageName cannot be null or empty");
319 
320             try {
321                 int state = getAppEcmState(packageName, userId);
322                 return state == EcmState.ECM_STATE_GUARDED_AND_ACKNOWLEDGED;
323             } catch (NameNotFoundException e) {
324                 throw new IllegalArgumentException(e);
325             }
326         }
327 
setClearRestrictionAllowed(@onNull String packageName, @UserIdInt int userId)328         public void setClearRestrictionAllowed(@NonNull String packageName, @UserIdInt int userId) {
329             enforcePermissions("setClearRestrictionAllowed", userId);
330             if (!UserUtils.isUserExistent(userId, getContext())) {
331                 return;
332             }
333 
334             Preconditions.checkStringNotEmpty(packageName, "packageName cannot be null or empty");
335 
336             try {
337                 if (isPackageEcmGuarded(packageName, userId)) {
338                     setAppEcmState(packageName, EcmState.ECM_STATE_GUARDED_AND_ACKNOWLEDGED,
339                             userId);
340                 }
341             } catch (NameNotFoundException e) {
342                 throw new IllegalArgumentException(e);
343             }
344         }
345 
enforcePermissions(@onNull String methodName, @UserIdInt int userId)346         private void enforcePermissions(@NonNull String methodName, @UserIdInt int userId) {
347             UserUtils.enforceCrossUserPermission(userId, /* allowAll= */ false,
348                     /* enforceForProfileGroup= */ false, methodName, mContext);
349             mContext.enforceCallingOrSelfPermission(
350                     android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES, methodName);
351         }
352 
isPackageEcmGuarded(@onNull String packageName, @UserIdInt int userId)353         private boolean isPackageEcmGuarded(@NonNull String packageName, @UserIdInt int userId)
354                 throws NameNotFoundException {
355             ApplicationInfo applicationInfo = getApplicationInfoAsUser(packageName, userId);
356             // Always trust allow-listed and pre-installed packages
357             if (isAllowlistedPackage(packageName) || isAllowlistedInstaller(packageName)
358                     || isPackagePreinstalled(applicationInfo)) {
359                 return false;
360             }
361 
362             // If the package already has an explicitly-set state, use that
363             @EcmState int ecmState = getAppEcmState(packageName, userId);
364             if (ecmState == EcmState.ECM_STATE_GUARDED
365                     || ecmState == EcmState.ECM_STATE_GUARDED_AND_ACKNOWLEDGED) {
366                 return true;
367             }
368             if (ecmState == EcmState.ECM_STATE_NOT_GUARDED) {
369                 return false;
370             }
371 
372             // Otherwise, lazily decide whether the app is considered guarded.
373             InstallSourceInfo installSource;
374             try {
375                 installSource = mContext.createContextAsUser(UserHandle.of(userId), 0)
376                         .getPackageManager()
377                         .getInstallSourceInfo(packageName);
378             } catch (NameNotFoundException e) {
379                 Log.w(LOG_TAG, "Package not found: " + packageName);
380                 return false;
381             }
382 
383             // These install sources are always considered dangerous.
384             // PackageInstallers that are trusted can use these as a signal that the
385             // packages they've installed aren't as trusted as themselves.
386             int packageSource = installSource.getPackageSource();
387             if (packageSource == PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE
388                     || packageSource == PackageInstaller.PACKAGE_SOURCE_DOWNLOADED_FILE) {
389                 return true;
390             }
391             String installingPackageName = installSource.getInstallingPackageName();
392             ApplicationInfo installingApplicationInfo =
393                     getApplicationInfoAsUser(installingPackageName, userId);
394 
395             // ECM doesn't consider a transitive chain of trust for install sources.
396             // If this package hasn't been explicitly handled by this point
397             // then it is exempt from ECM if the immediate parent is a trusted installer
398             return !(trustPackagesInstalledViaNonAllowlistedInstallers()
399                     || isPackagePreinstalled(installingApplicationInfo)
400                     || isAllowlistedInstaller(installingPackageName));
401         }
402 
isSettingEcmGuardedForPackage(@onNull String settingIdentifier, @NonNull String packageName, @UserIdInt int userId)403         private boolean isSettingEcmGuardedForPackage(@NonNull String settingIdentifier,
404                 @NonNull String packageName, @UserIdInt int userId) throws NameNotFoundException {
405             if (!PER_PACKAGE_PROTECTED_SETTINGS.contains(settingIdentifier)) {
406                 return false;
407             }
408             return isPackageEcmGuarded(packageName, userId);
409         }
410 
isAllowlistedPackage(String packageName)411         private boolean isAllowlistedPackage(String packageName) {
412             return isPackageSignedWithAnyOf(packageName,
413                     mTrustedPackageCertDigests.get(packageName));
414         }
415 
isAllowlistedInstaller(String packageName)416         private boolean isAllowlistedInstaller(String packageName) {
417             return isPackageSignedWithAnyOf(packageName,
418                     mTrustedInstallerCertDigests.get(packageName));
419         }
420 
isPackageSignedWithAnyOf(String packageName, List<byte[]> certDigests)421         private boolean isPackageSignedWithAnyOf(String packageName, List<byte[]> certDigests) {
422             if (packageName != null && certDigests != null) {
423                 for (int i = 0, count = certDigests.size(); i < count; i++) {
424                     byte[] trustedCertDigest = certDigests.get(i);
425                     if (mPackageManager.hasSigningCertificate(packageName, trustedCertDigest,
426                             PackageManager.CERT_INPUT_SHA256)) {
427                         return true;
428                     }
429                 }
430             }
431             return false;
432         }
433 
434         /**
435          * @return {@code true} if zero {@code <enhanced-confirmation-trusted-installer>} entries
436          * are defined in {@code frameworks/base/data/etc/enhanced-confirmation.xml}; in this case,
437          * we treat all installers as trusted.
438          */
trustPackagesInstalledViaNonAllowlistedInstallers()439         private boolean trustPackagesInstalledViaNonAllowlistedInstallers() {
440             return mTrustedInstallerCertDigests.isEmpty();
441         }
442 
isPackagePreinstalled(@ullable ApplicationInfo applicationInfo)443         private boolean isPackagePreinstalled(@Nullable ApplicationInfo applicationInfo) {
444             if (applicationInfo == null) {
445                 return false;
446             }
447             return (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
448         }
449 
450         @SuppressLint("WrongConstant")
setAppEcmState(@onNull String packageName, @EcmState int ecmState, @UserIdInt int userId)451         private void setAppEcmState(@NonNull String packageName, @EcmState int ecmState,
452                 @UserIdInt int userId) throws NameNotFoundException {
453             int packageUid = getPackageUid(mPackageManager, packageName, userId);
454             final long identityToken = Binder.clearCallingIdentity();
455             try {
456                 mAppOpsManager.setMode(AppOpsManager.OPSTR_ACCESS_RESTRICTED_SETTINGS, packageUid,
457                         packageName, ecmState);
458             } finally {
459                 Binder.restoreCallingIdentity(identityToken);
460             }
461         }
462 
getAppEcmState(@onNull String packageName, @UserIdInt int userId)463         private @EcmState int getAppEcmState(@NonNull String packageName, @UserIdInt int userId)
464                 throws NameNotFoundException {
465             int packageUid = getPackageUid(mPackageManager, packageName, userId);
466             final long identityToken = Binder.clearCallingIdentity();
467             try {
468                 return mAppOpsManager.noteOpNoThrow(AppOpsManager.OPSTR_ACCESS_RESTRICTED_SETTINGS,
469                         packageUid, packageName, mAttributionTag, /* message */ null);
470             } finally {
471                 Binder.restoreCallingIdentity(identityToken);
472             }
473         }
474 
isSettingEcmProtected(@onNull String settingIdentifier)475         private boolean isSettingEcmProtected(@NonNull String settingIdentifier) {
476             if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
477                     || mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
478                 return false;
479             }
480 
481             if (PER_PACKAGE_PROTECTED_SETTINGS.contains(settingIdentifier)) {
482                 return true;
483             }
484             if (UNTRUSTED_CALL_RESTRICTED_SETTINGS.contains(settingIdentifier)) {
485                 return true;
486             }
487             // TODO(b/310218979): Add role selections as protected settings
488             return false;
489         }
490 
491         // Generate a global protection reason for why the setting may be blocked. Note, this
492         // method will result in a metric being logged, representing a blocked/allowed setting
getGlobalProtectionReason(@onNull String settingIdentifier, @NonNull String packageName, @UserIdInt int userId)493         private String getGlobalProtectionReason(@NonNull String settingIdentifier,
494                 @NonNull String packageName, @UserIdInt int userId) {
495             if (!UNTRUSTED_CALL_RESTRICTED_SETTINGS.contains(settingIdentifier)) {
496                 return null;
497             }
498             if (mCallTracker == null) {
499                 return null;
500             }
501             String reason = null;
502             if (mCallTracker.isUntrustedCallOngoing()) {
503                 if (!AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE.equals(settingIdentifier)) {
504                     reason = REASON_PHONE_STATE;
505                 }
506                 if (!isAccessibilityTool(packageName, userId)) {
507                     reason = REASON_PHONE_STATE;
508                 }
509             }
510             mCallTracker.onEcmInteraction(packageName, userId, settingIdentifier, reason == null);
511 
512             return reason;
513         }
514 
isAccessibilityTool(@onNull String packageName, @UserIdInt int userId)515         private boolean isAccessibilityTool(@NonNull String packageName, @UserIdInt int userId) {
516             AccessibilityManager am;
517             synchronized (mUserAccessibilityManagers) {
518                 if (!mUserAccessibilityManagers.containsKey(userId)) {
519                     Context userContext =
520                             getContext().createContextAsUser(UserHandle.of(userId), 0);
521                     mUserAccessibilityManagers.put(userId, userContext.getSystemService(
522                             AccessibilityManager.class));
523                 }
524                 am = mUserAccessibilityManagers.get(userId);
525             }
526             List<AccessibilityServiceInfo> infos = am.getInstalledAccessibilityServiceList();
527             for (int i = 0; i < infos.size(); i++) {
528                 AccessibilityServiceInfo info = infos.get(i);
529                 String servicePackageName = null;
530                 if (info.getResolveInfo() != null && info.getResolveInfo().serviceInfo != null) {
531                     servicePackageName = info.getResolveInfo().serviceInfo.packageName;
532                 }
533                 if (packageName.equals(servicePackageName)) {
534                     return info.isAccessibilityTool();
535                 }
536             }
537             return false;
538         }
539 
540         @Nullable
getApplicationInfoAsUser(@ullable String packageName, @UserIdInt int userId)541         private ApplicationInfo getApplicationInfoAsUser(@Nullable String packageName,
542                 @UserIdInt int userId) {
543             if (packageName == null) {
544                 Log.w(LOG_TAG, "The packageName should not be null.");
545                 return null;
546             }
547             try {
548                 return mPackageManager.getApplicationInfoAsUser(packageName, /* flags */ 0,
549                         UserHandle.of(userId));
550             } catch (NameNotFoundException e) {
551                 Log.w(LOG_TAG, "Package not found: " + packageName, e);
552                 return null;
553             }
554         }
555     }
556 
557     private static class CallTracker {
558         // The time we will remember an untrusted call
559         private static final long UNTRUSTED_CALL_STORAGE_TIME_MS = TimeUnit.HOURS.toMillis(1);
560         // The minimum time that must pass between individual logs of the same call, uid, trusted
561         // status, and allowed setting.
562         private static final long MAX_LOGGING_FREQUENCY_MS = TimeUnit.SECONDS.toMillis(30);
563         // A map of call ID to ongoing or recently removed calls. Concurrent because
564         // additions/removals happen on background threads, but queries on main thread.
565         private final Map<String, TrackedCall> mCalls = new ConcurrentHashMap<>();
566 
567         // A cache of hashed callers, uids, trusted status, and allowed status. Ensures that we
568         // do not log the same interaction too many times
569         private final Map<Integer, Long> mLogCache = new ConcurrentHashMap<>();
570 
571         private class TrackedCall {
572             public @CallType Integer callType;
573             public String caller;
574 
575             public long startTime = SystemClock.elapsedRealtime();
576 
577             public long endTime = -1;
578 
579             public boolean incoming;
580 
581             public boolean blockedDuringCall = false;
582 
583             public boolean ecmInteractionDuringCall = false;
584 
isFinished()585             public boolean isFinished() {
586                 return endTime > 0;
587             }
588 
TrackedCall(@onNull Call call)589             TrackedCall(@NonNull Call call) {
590                 caller = getPhoneNumber(call);
591                 if (caller == null) {
592                     caller = getDisplayName(call);
593                 }
594                 callType = getCallType(call);
595                 incoming = call.getDetails().getCallDirection() == Call.Details.DIRECTION_INCOMING;
596             }
597         }
598 
599         private Context mContext;
600         private TelephonyManager mTelephonyManager;
601         private PackageManager mPackageManager;
602 
CallTracker(Context context)603         CallTracker(Context context) {
604             mContext = context;
605             mTelephonyManager = context.getSystemService(TelephonyManager.class);
606             mPackageManager = context.getPackageManager();
607         }
608 
addCall(@onNull Call call)609         public void addCall(@NonNull Call call) {
610             if (call.getDetails() == null) {
611                 return;
612             }
613             pruneOldFinishedCalls();
614             pruneOldLogs();
615             mCalls.put(call.getDetails().getId(), new TrackedCall(call));
616         }
617 
endCall(@onNull String callId)618         public void endCall(@NonNull String callId) {
619             TrackedCall trackedCall = mCalls.get(callId);
620             if (trackedCall == null) {
621                 // TODO b/379941144: Capture a bug report whenever this happens.
622                 return;
623             }
624             if (trackedCall.isFinished()) {
625                 return;
626             }
627             if (!Flags.unknownCallSettingBlockedLoggingEnabled()) {
628                 mCalls.remove(callId);
629                 return;
630             }
631 
632             trackedCall.endTime = SystemClock.elapsedRealtime();
633             if (trackedCall.callType != CALL_TYPE_UNTRUSTED) {
634                 // We only hang onto a finished call if the call was untrusted
635                 mCalls.remove(callId);
636             }
637 
638             if (trackedCall.ecmInteractionDuringCall) {
639                 long duration = TimeUnit.MILLISECONDS.toSeconds(
640                         trackedCall.endTime - trackedCall.startTime);
641                 int durationInt =  (int) Math.min(duration, Integer.MAX_VALUE);
642                 PermissionControllerStatsLog.write(CALL_WITH_ECM_INTERACTION_REPORTED,
643                         trackedCall.blockedDuringCall, durationInt);
644             }
645 
646             pruneOldFinishedCalls();
647             pruneOldLogs();
648         }
649 
endAllCalls()650         public void endAllCalls() {
651             for (String callId: mCalls.keySet()) {
652                 endCall(callId);
653             }
654         }
655 
onEcmInteraction(@onNull String packageName, int userId, @NonNull String settingIdentifier, boolean allowed)656         public void onEcmInteraction(@NonNull String packageName, int userId,
657                 @NonNull String settingIdentifier, boolean allowed) {
658             if (!Flags.unknownCallSettingBlockedLoggingEnabled()) {
659                 return;
660             }
661 
662             boolean hasOngoingCall = false;
663             for (TrackedCall current: mCalls.values()) {
664                 if (current.isFinished()) {
665                     // We only care about ongoing calls
666                     continue;
667                 }
668                 hasOngoingCall = true;
669                 // Mark that the current call had a setting interaction during it
670                 current.ecmInteractionDuringCall = true;
671                 current.blockedDuringCall = !allowed;
672                 logInCallRestrictionEvent(packageName, userId, settingIdentifier, allowed, current);
673             }
674             if (!hasOngoingCall) {
675                 logInCallRestrictionEvent(packageName, userId, settingIdentifier, allowed, null);
676             }
677 
678         }
679 
getCallType(@onNull Call call)680         private @CallType int getCallType(@NonNull Call call) {
681             String number = getPhoneNumber(call);
682             try {
683                 if (number != null && mTelephonyManager.isEmergencyNumber(number)) {
684                     return CALL_TYPE_EMERGENCY;
685                 }
686             } catch (RuntimeException e) {
687                 // If either of these are thrown, the telephony service is not available on the
688                 // current device, either because the device lacks telephony calling, or the
689                 // telephony service is unavailable.
690             }
691             UserHandle user = mContext.getUser();
692             /*
693             TODO b/408470449: reenable once call extras are fixed
694             Bundle extras = call.getDetails().getExtras();
695             if (extras != null) {
696                 user = extras.getParcelable(Intent.EXTRA_USER_HANDLE, UserHandle.class);
697             }
698              */
699             if (number != null) {
700                 return hasContactWithPhoneNumber(number, user)
701                         ? CALL_TYPE_TRUSTED : CALL_TYPE_UNTRUSTED;
702             } else {
703                 return hasContactWithDisplayName(getDisplayName(call), user)
704                         ? CALL_TYPE_TRUSTED : CALL_TYPE_UNTRUSTED;
705             }
706         }
707 
getPhoneNumber(@onNull Call call)708         private static String getPhoneNumber(@NonNull Call call) {
709             Uri handle = call.getDetails().getHandle();
710             if (handle == null || handle.getScheme() == null) {
711                 return null;
712             }
713             if (!handle.getScheme().equals(PhoneAccount.SCHEME_TEL)) {
714                 return null;
715             }
716             return handle.getSchemeSpecificPart();
717         }
718 
getDisplayName(@onNull Call call)719         private static String getDisplayName(@NonNull Call call) {
720             return call.getDetails().getCallerDisplayName();
721         }
722 
hasContactWithPhoneNumber(@ullable String phoneNumber, UserHandle user)723         private boolean hasContactWithPhoneNumber(@Nullable String phoneNumber, UserHandle user) {
724             if (phoneNumber == null) {
725                 return false;
726             }
727             Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI,
728                     Uri.encode(phoneNumber));
729             String[] projection = new String[]{
730                     PhoneLookup.DISPLAY_NAME,
731                     ContactsContract.PhoneLookup._ID
732             };
733             try (Cursor res = getUserContentResolver(user).query(uri, projection, null, null)) {
734                 return res != null && res.getCount() > 0;
735             }
736         }
737 
hasContactWithDisplayName(@ullable String displayName, UserHandle user)738         private boolean hasContactWithDisplayName(@Nullable String displayName, UserHandle user) {
739             if (displayName == null) {
740                 return false;
741             }
742             Uri uri = ContactsContract.Data.CONTENT_URI;
743             String[] projection = new String[]{PhoneLookup._ID};
744             String selection = StructuredName.DISPLAY_NAME + " = ?";
745             String[] selectionArgs = new String[]{displayName};
746             try (Cursor res = getUserContentResolver(user)
747                          .query(uri, projection, selection, selectionArgs, null)) {
748                 return res != null && res.getCount() > 0;
749             }
750         }
751 
getUserContentResolver(UserHandle user)752         private ContentResolver getUserContentResolver(UserHandle user) {
753             return mContext.createContextAsUser(user, 0).getContentResolver();
754         }
755 
getOngoingCallOfType(@allType int callType)756         private TrackedCall getOngoingCallOfType(@CallType int callType) {
757             for (TrackedCall call : mCalls.values()) {
758                 if (!call.isFinished() && call.callType == callType) {
759                     return call;
760                 }
761             }
762             return null;
763         }
764 
isUntrustedCallOngoing()765         public boolean isUntrustedCallOngoing() {
766             if (!Flags.unknownCallPackageInstallBlockingEnabled()) {
767                 return false;
768             }
769 
770             if (getOngoingCallOfType(CALL_TYPE_EMERGENCY) != null) {
771                 // If we have an emergency call, return false always.
772                 return false;
773             }
774             return getOngoingCallOfType(CALL_TYPE_UNTRUSTED) != null;
775         }
776 
pruneOldFinishedCalls()777         void pruneOldFinishedCalls() {
778             if (!Flags.unknownCallSettingBlockedLoggingEnabled()) {
779                 return;
780             }
781             long cutoff = SystemClock.elapsedRealtime() - UNTRUSTED_CALL_STORAGE_TIME_MS;
782             mCalls.entrySet().removeIf(
783                     e -> e.getValue().isFinished() && e.getValue().endTime < cutoff);
784         }
785 
pruneOldLogs()786         void pruneOldLogs() {
787             if (!Flags.unknownCallSettingBlockedLoggingEnabled()) {
788                 return;
789             }
790             long cutoff = SystemClock.elapsedRealtime() - MAX_LOGGING_FREQUENCY_MS;
791             mLogCache.entrySet().removeIf(e -> e.getValue() < cutoff);
792         }
793 
logInCallRestrictionEvent(@onNull String packageName, int userId, @NonNull String settingIdentifier, boolean allowed, @Nullable TrackedCall call)794         private void logInCallRestrictionEvent(@NonNull String packageName, int userId,
795                 @NonNull String settingIdentifier, boolean allowed, @Nullable TrackedCall call) {
796             if (!Flags.unknownCallSettingBlockedLoggingEnabled()) {
797                 return;
798             }
799 
800             if (!UNTRUSTED_CALL_RESTRICTED_SETTINGS.contains(settingIdentifier)) {
801                 return;
802             }
803 
804             int uid;
805             try {
806                 uid = mPackageManager.getPackageUid(packageName, userId);
807             } catch (NameNotFoundException e) {
808                 return;
809             }
810 
811             boolean callInProgress = call != null && !call.isFinished();
812             boolean trusted = true;
813             boolean incoming = false;
814             boolean callBackAfterBlock = false;
815             if (callInProgress) {
816                 trusted = call.callType != CALL_TYPE_UNTRUSTED;
817                 incoming = call.incoming;
818 
819                 // Look for a previous call from the same caller, that had a blocked ecm interaction
820                 for (TrackedCall otherCall : mCalls.values()) {
821                     if (!otherCall.isFinished()) {
822                         continue;
823                     }
824                     if (!Objects.equals(otherCall.caller, call.caller)) {
825                         continue;
826                     }
827                     if (!otherCall.blockedDuringCall) {
828                         continue;
829                     }
830                     callBackAfterBlock = true;
831                 }
832             }
833 
834             String caller = callInProgress ? call.caller : null;
835             int logHash = Objects.hash(caller, uid, settingIdentifier, allowed, trusted);
836             Long lastLogTime = mLogCache.get(logHash);
837             long now = SystemClock.elapsedRealtime();
838             long cutoff = now - MAX_LOGGING_FREQUENCY_MS;
839             if (lastLogTime != null && lastLogTime > cutoff) {
840                 return;
841             }
842             mLogCache.put(logHash, now);
843 
844             PermissionControllerStatsLog.write(ECM_RESTRICTION_QUERY_IN_CALL_REPORTED, uid,
845                     settingIdentifier, allowed, callInProgress, incoming, trusted,
846                     callBackAfterBlock);
847         }
848     }
849 }
850