1 /*
2  * Copyright 2014, 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.server.telecom;
18 
19 import static android.provider.CallLog.AddCallParams.AddCallParametersBuilder.MAX_NUMBER_OF_CHARACTERS;
20 import static android.provider.CallLog.Calls.BLOCK_REASON_NOT_BLOCKED;
21 import static android.telephony.CarrierConfigManager.KEY_SUPPORT_IMS_CONFERENCE_EVENT_PACKAGE_BOOL;
22 
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.content.ContentResolver;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.pm.PackageManager;
29 import android.database.Cursor;
30 import android.location.Country;
31 import android.location.CountryDetector;
32 import android.location.Location;
33 import android.net.Uri;
34 import android.os.AsyncTask;
35 import android.os.Bundle;
36 import android.os.Handler;
37 import android.os.HandlerExecutor;
38 import android.os.Looper;
39 import android.os.UserHandle;
40 import android.os.PersistableBundle;
41 import android.os.UserManager;
42 import android.provider.CallLog;
43 import android.provider.CallLog.Calls;
44 import android.telecom.Connection;
45 import android.telecom.DisconnectCause;
46 import android.telecom.Log;
47 import android.telecom.PhoneAccount;
48 import android.telecom.PhoneAccountHandle;
49 import android.telecom.TelecomManager;
50 import android.telecom.VideoProfile;
51 import android.telephony.CarrierConfigManager;
52 import android.telephony.PhoneNumberUtils;
53 import android.telephony.SubscriptionManager;
54 import android.util.Pair;
55 
56 import com.android.internal.annotations.VisibleForTesting;
57 import com.android.server.telecom.callfiltering.CallFilteringResult;
58 import com.android.server.telecom.flags.FeatureFlags;
59 import com.android.server.telecom.flags.Flags;
60 
61 import java.util.Arrays;
62 import java.util.Locale;
63 import java.util.Objects;
64 import java.util.UUID;
65 import java.util.stream.Stream;
66 
67 /**
68  * Helper class that provides functionality to write information about calls and their associated
69  * caller details to the call log. All logging activity will be performed asynchronously in a
70  * background thread to avoid blocking on the main thread.
71  */
72 @VisibleForTesting
73 public final class CallLogManager extends CallsManagerListenerBase {
74 
75     public interface LogCallCompletedListener {
onLogCompleted(@ullable Uri uri)76         void onLogCompleted(@Nullable Uri uri);
77     }
78 
79     /**
80      * Parameter object to hold the arguments to add a call in the call log DB.
81      */
82     private static class AddCallArgs {
AddCallArgs(Context context, CallLog.AddCallParams params, @Nullable LogCallCompletedListener logCallCompletedListener, @NonNull Call call)83         public AddCallArgs(Context context, CallLog.AddCallParams params,
84                 @Nullable LogCallCompletedListener logCallCompletedListener,
85                 @NonNull Call call) {
86             this.context = context;
87             this.params = params;
88             this.logCallCompletedListener = logCallCompletedListener;
89             this.call = call;
90 
91         }
92         // Since the members are accessed directly, we don't use the
93         // mXxxx notation.
94         public final Context context;
95         public final CallLog.AddCallParams params;
96         public final Call call;
97         @Nullable
98         public final LogCallCompletedListener logCallCompletedListener;
99     }
100 
101     private static final String TAG = CallLogManager.class.getSimpleName();
102 
103     // Copied from android.telephony.DisconnectCause.toString
104     // TODO: come up with a better way to indicate in a android.telecom.DisconnectCause that
105     // a conference was merged successfully
106     private static final String REASON_IMS_MERGED_SUCCESSFULLY = "IMS_MERGED_SUCCESSFULLY";
107     private static final UUID LOG_CALL_FAILED_ANOMALY_ID =
108             UUID.fromString("d9b38771-ff36-417b-8723-2363a870c702");
109     private static final String LOG_CALL_FAILED_ANOMALY_DESC =
110             "Based on the current user, Telecom detected failure to record a call to the call log.";
111 
112     private final Context mContext;
113     private final CarrierConfigManager mCarrierConfigManager;
114     private final PhoneAccountRegistrar mPhoneAccountRegistrar;
115     private final MissedCallNotifier mMissedCallNotifier;
116     private AnomalyReporterAdapter mAnomalyReporterAdapter;
117     private static final String ACTION_CALLS_TABLE_ADD_ENTRY =
118             "com.android.server.telecom.intent.action.CALLS_ADD_ENTRY";
119     private static final String PERMISSION_PROCESS_CALLLOG_INFO =
120             "android.permission.PROCESS_CALLLOG_INFO";
121     private static final String CALL_TYPE = "callType";
122     private static final String CALL_DURATION = "duration";
123 
124     private final Object mLock = new Object();
125     private Country mCurrentCountry;
126     private String mCurrentCountryIso;
127     private HandlerExecutor mCountryCodeExecutor;
128 
129     private final FeatureFlags mFeatureFlags;
130 
CallLogManager(Context context, PhoneAccountRegistrar phoneAccountRegistrar, MissedCallNotifier missedCallNotifier, AnomalyReporterAdapter anomalyReporterAdapter, FeatureFlags featureFlags)131     public CallLogManager(Context context, PhoneAccountRegistrar phoneAccountRegistrar,
132             MissedCallNotifier missedCallNotifier, AnomalyReporterAdapter anomalyReporterAdapter,
133             FeatureFlags featureFlags) {
134         mContext = context;
135         mCarrierConfigManager = (CarrierConfigManager) mContext
136                 .getSystemService(Context.CARRIER_CONFIG_SERVICE);
137         mPhoneAccountRegistrar = phoneAccountRegistrar;
138         mMissedCallNotifier = missedCallNotifier;
139         mAnomalyReporterAdapter = anomalyReporterAdapter;
140         mCountryCodeExecutor = new HandlerExecutor(new Handler(Looper.getMainLooper()));
141         mFeatureFlags = featureFlags;
142     }
143 
144     @Override
onCallStateChanged(Call call, int oldState, int newState)145     public void onCallStateChanged(Call call, int oldState, int newState) {
146         int disconnectCause = call.getDisconnectCause().getCode();
147         boolean isNewlyDisconnected =
148                 newState == CallState.DISCONNECTED || newState == CallState.ABORTED;
149         boolean isCallCanceled = isNewlyDisconnected && disconnectCause == DisconnectCause.CANCELED;
150 
151         if (!isNewlyDisconnected) {
152             return;
153         }
154 
155         if (shouldLogDisconnectedCall(call, oldState, isCallCanceled)) {
156             int type;
157             if (!call.isIncoming()) {
158                 type = Calls.OUTGOING_TYPE;
159             } else if (disconnectCause == DisconnectCause.MISSED) {
160                 type = Calls.MISSED_TYPE;
161             } else if (disconnectCause == DisconnectCause.ANSWERED_ELSEWHERE) {
162                 type = Calls.ANSWERED_EXTERNALLY_TYPE;
163             } else if (disconnectCause == DisconnectCause.REJECTED) {
164                 type = Calls.REJECTED_TYPE;
165             } else {
166                 type = Calls.INCOMING_TYPE;
167             }
168             // Always show the notification for managed calls. For self-managed calls, it is up to
169             // the app to show the notification, so suppress the notification when logging the call.
170             boolean showNotification = !call.isSelfManaged();
171             logCall(call, type, showNotification, null /*result*/);
172         }
173     }
174 
175     /**
176      * Log newly disconnected calls only if all of below conditions are met:
177      * Call was NOT in the "choose account" phase when disconnected
178      * Call is NOT a conference call which had children (unless it was remotely hosted).
179      * Call is NOT a child call from a conference which was remotely hosted.
180      * Call has NOT indicated it should be skipped for logging in its extras
181      * Call is NOT simulating a single party conference.
182      * Call was NOT explicitly canceled, except for disconnecting from a conference.
183      * Call is NOT an external call or an external call on watch.
184      * Call is NOT disconnected because of merging into a conference.
185      * Call is NOT a self-managed call OR call is a self-managed call which has indicated it
186      * should be logged in its PhoneAccount
187      */
188     @VisibleForTesting
shouldLogDisconnectedCall(Call call, int oldState, boolean isCallCanceled)189     public boolean shouldLogDisconnectedCall(Call call, int oldState, boolean isCallCanceled) {
190         boolean shouldCallSelfManagedLogged = call.isLoggedSelfManaged()
191                 && (call.getHandoverState() == HandoverState.HANDOVER_NONE
192                 || call.getHandoverState() == HandoverState.HANDOVER_COMPLETE);
193 
194         // "Choose account" phase when disconnected
195         if (oldState == CallState.SELECT_PHONE_ACCOUNT) {
196             return false;
197         }
198         // A conference call which had children should not be logged, unless it was remotely hosted.
199         if (call.isConference() && call.hadChildren() &&
200                 !call.hasProperty(Connection.PROPERTY_REMOTELY_HOSTED)) {
201             return false;
202         }
203 
204         // A conference call which had no children should not be logged; this case will occur on IMS
205         // when no conference event package data is received.  We will have logged the participants
206         // as they merge into the conference, so we should not log the conference itself.
207         if (call.isConference() && !call.hadChildren() &&
208                 !call.hasProperty(Connection.PROPERTY_REMOTELY_HOSTED)) {
209             return false;
210         }
211 
212         if (mFeatureFlags.telecomSkipLogBasedOnExtra() && call.getExtras() != null
213                 && call.getExtras().containsKey(TelecomManager.EXTRA_DO_NOT_LOG_CALL)) {
214             return false;
215         }
216 
217         // A child call of a conference which was remotely hosted; these didn't originate on this
218         // device and should not be logged.
219         if (call.getParentCall() != null && call.hasProperty(Connection.PROPERTY_REMOTELY_HOSTED)) {
220             return false;
221         }
222 
223         DisconnectCause cause = call.getDisconnectCause();
224         if (isCallCanceled) {
225             // No log when disconnecting to simulate a single party conference.
226             if (cause != null
227                     && DisconnectCause.REASON_EMULATING_SINGLE_CALL.equals(cause.getReason())) {
228                 return false;
229             }
230             // Explicitly canceled
231             // Conference children connections only have CAPABILITY_DISCONNECT_FROM_CONFERENCE.
232             // Log them when they are disconnected from conference.
233             return (call.getConnectionCapabilities()
234                     & Connection.CAPABILITY_DISCONNECT_FROM_CONFERENCE)
235                     == Connection.CAPABILITY_DISCONNECT_FROM_CONFERENCE;
236         }
237         // An external and non-watch call
238         if (call.isExternalCall() && (!mContext.getPackageManager().hasSystemFeature(
239                 PackageManager.FEATURE_WATCH)
240                 || !mFeatureFlags.telecomLogExternalWearableCalls())) {
241             return false;
242         }
243 
244         // Call merged into conferences and marked with IMS_MERGED_SUCCESSFULLY.
245         // Return false if the conference supports the participants packets for the carrier.
246         // Otherwise, fall through. Merged calls would be associated with disconnected
247         // connections because of special carrier requirements. Those calls don't look like
248         // merged, e.g. could be one active and the other on hold.
249         if (cause != null && REASON_IMS_MERGED_SUCCESSFULLY.equals(cause.getReason())) {
250             int subscriptionId = mPhoneAccountRegistrar
251                     .getSubscriptionIdForPhoneAccount(call.getTargetPhoneAccount());
252             // By default, the conference should return a list of participants.
253             if (subscriptionId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
254                 return false;
255             }
256 
257             if (mCarrierConfigManager == null) {
258                 return false;
259             }
260             PersistableBundle b = mCarrierConfigManager.getConfigForSubId(subscriptionId);
261             if (b == null || b.getBoolean(KEY_SUPPORT_IMS_CONFERENCE_EVENT_PACKAGE_BOOL, true)) {
262                 return false;
263             }
264         }
265 
266         // Call is NOT a self-managed call OR call is a self-managed call which has indicated it
267         // should be logged in its PhoneAccount
268         return !call.isSelfManaged() || shouldCallSelfManagedLogged;
269     }
270 
logCall(Call call, int type, boolean showNotificationForMissedCall, CallFilteringResult result)271     void logCall(Call call, int type, boolean showNotificationForMissedCall, CallFilteringResult
272             result) {
273         if ((type == Calls.MISSED_TYPE || type == Calls.BLOCKED_TYPE) &&
274                 showNotificationForMissedCall) {
275             logCall(call, type, new LogCallCompletedListener() {
276                 @Override
277                 public void onLogCompleted(@Nullable Uri uri) {
278                     if (mFeatureFlags.addCallUriForMissedCalls()){
279                         mMissedCallNotifier.showMissedCallNotification(
280                                 new MissedCallNotifier.CallInfo(call), uri);
281                     } else {
282                         mMissedCallNotifier.showMissedCallNotification(
283                                 new MissedCallNotifier.CallInfo(call), /* uri= */ null);
284                     }
285                 }
286             }, result);
287         } else {
288             logCall(call, type, null, result);
289         }
290     }
291 
292     /**
293      * Logs a call to the call log based on the {@link Call} object passed in.
294      *
295      * @param call The call object being logged
296      * @param callLogType The type of call log entry to log this call as. See:
297      *     {@link android.provider.CallLog.Calls#INCOMING_TYPE}
298      *     {@link android.provider.CallLog.Calls#OUTGOING_TYPE}
299      *     {@link android.provider.CallLog.Calls#MISSED_TYPE}
300      *     {@link android.provider.CallLog.Calls#BLOCKED_TYPE}
301      * @param logCallCompletedListener optional callback called after the call is logged.
302      * @param result is generated when call type is
303      *     {@link android.provider.CallLog.Calls#BLOCKED_TYPE}.
304      */
logCall(Call call, int callLogType, @Nullable LogCallCompletedListener logCallCompletedListener, CallFilteringResult result)305     void logCall(Call call, int callLogType,
306             @Nullable LogCallCompletedListener logCallCompletedListener, CallFilteringResult result) {
307 
308         CallLog.AddCallParams.AddCallParametersBuilder paramBuilder =
309                 new CallLog.AddCallParams.AddCallParametersBuilder();
310         if (call.getConnectTimeMillis() != 0
311                 && call.getConnectTimeMillis() < call.getCreationTimeMillis()) {
312             // If connected time is available, use connected time. The connected time might be
313             // earlier than created time since it might come from carrier sent special SMS to
314             // notifier user earlier missed call.
315             paramBuilder.setStart(call.getConnectTimeMillis());
316         } else {
317             paramBuilder.setStart(call.getCreationTimeMillis());
318         }
319 
320         paramBuilder.setDuration((int) (call.getAgeMillis() / 1000));
321 
322         String logNumber = getLogNumber(call);
323         paramBuilder.setNumber(logNumber);
324 
325         Log.d(TAG, "logNumber set to: %s", Log.pii(logNumber));
326 
327         String formattedViaNumber = PhoneNumberUtils.formatNumber(call.getViaNumber(),
328                 getCountryIso());
329         formattedViaNumber = (formattedViaNumber != null) ?
330                 formattedViaNumber : call.getViaNumber();
331         paramBuilder.setViaNumber(formattedViaNumber);
332 
333         final PhoneAccountHandle emergencyAccountHandle =
334                 TelephonyUtil.getDefaultEmergencyPhoneAccount().getAccountHandle();
335         PhoneAccountHandle accountHandle = call.getTargetPhoneAccount();
336         if (emergencyAccountHandle.equals(accountHandle)) {
337             accountHandle = null;
338         }
339         paramBuilder.setAccountHandle(accountHandle);
340 
341         paramBuilder.setDataUsage(call.getCallDataUsage() == Call.DATA_USAGE_NOT_SET
342                 ? Long.MIN_VALUE : call.getCallDataUsage());
343 
344         paramBuilder.setFeatures(getCallFeatures(call.getVideoStateHistory(),
345                 call.getDisconnectCause().getCode() == DisconnectCause.CALL_PULLED,
346                 call.wasHighDefAudio(), call.wasWifi(),
347                 (call.getConnectionProperties() & Connection.PROPERTY_ASSISTED_DIALING) ==
348                         Connection.PROPERTY_ASSISTED_DIALING,
349                 call.wasEverRttCall(),
350                 call.wasVolte()));
351 
352         if (result == null) {
353             result = new CallFilteringResult.Builder()
354                     .setCallScreeningAppName(call.getCallScreeningAppName())
355                     .setCallScreeningComponentName(call.getCallScreeningComponentName())
356                     .build();
357         }
358         if (callLogType == Calls.BLOCKED_TYPE || callLogType == Calls.MISSED_TYPE) {
359             paramBuilder.setCallBlockReason(result.mCallBlockReason);
360             paramBuilder.setCallScreeningComponentName(result.mCallScreeningComponentName);
361             paramBuilder.setCallScreeningAppName(result.mCallScreeningAppName);
362         } else {
363             paramBuilder.setCallBlockReason(BLOCK_REASON_NOT_BLOCKED);
364         }
365 
366         PhoneAccount phoneAccount = mPhoneAccountRegistrar.getPhoneAccountUnchecked(accountHandle);
367         UserHandle initiatingUser = call.getAssociatedUser();
368         if (phoneAccount != null &&
369                 phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) {
370             if (initiatingUser != null &&
371                     UserUtil.isProfile(mContext, initiatingUser, mFeatureFlags)) {
372                 paramBuilder.setUserToBeInsertedTo(initiatingUser);
373                 paramBuilder.setAddForAllUsers(false);
374             } else {
375                 paramBuilder.setAddForAllUsers(true);
376             }
377         } else {
378             if (accountHandle == null) {
379                 paramBuilder.setAddForAllUsers(true);
380             } else {
381                 paramBuilder.setUserToBeInsertedTo(accountHandle.getUserHandle());
382                 paramBuilder.setAddForAllUsers(accountHandle.getUserHandle() == null);
383             }
384         }
385         if (call.getIntentExtras() != null) {
386             if (call.getIntentExtras().containsKey(TelecomManager.EXTRA_PRIORITY)) {
387                 paramBuilder.setPriority(call.getIntentExtras()
388                         .getInt(TelecomManager.EXTRA_PRIORITY));
389             }
390             if (call.getIntentExtras().containsKey(TelecomManager.EXTRA_CALL_SUBJECT)) {
391                 paramBuilder.setSubject(call.getIntentExtras()
392                         .getString(TelecomManager.EXTRA_CALL_SUBJECT));
393             }
394             if (call.getIntentExtras().containsKey(TelecomManager.EXTRA_PICTURE_URI)) {
395                 paramBuilder.setPictureUri(call.getIntentExtras()
396                         .getParcelable(TelecomManager.EXTRA_PICTURE_URI));
397             }
398             // The picture uri can end up either in extras or in intent extras due to how these
399             // two bundles are set. For incoming calls they're in extras, but for outgoing calls
400             // they're in intentExtras.
401             if (call.getExtras() != null
402                     && call.getExtras().containsKey(TelecomManager.EXTRA_PICTURE_URI)) {
403                 paramBuilder.setPictureUri(call.getExtras()
404                         .getParcelable(TelecomManager.EXTRA_PICTURE_URI));
405             }
406             if (call.getIntentExtras().containsKey(TelecomManager.EXTRA_LOCATION)) {
407                 Location l = call.getIntentExtras().getParcelable(TelecomManager.EXTRA_LOCATION);
408                 if (l != null) {
409                     paramBuilder.setLatitude(l.getLatitude());
410                     paramBuilder.setLongitude(l.getLongitude());
411                 }
412             }
413         }
414 
415         paramBuilder.setCallerInfo(call.getCallerInfo());
416         paramBuilder.setPostDialDigits(call.getPostDialDigits());
417         paramBuilder.setPresentation(call.getHandlePresentation());
418         paramBuilder.setCallType(callLogType);
419         paramBuilder.setIsRead(call.isSelfManaged());
420         paramBuilder.setMissedReason(call.getMissedReason());
421         if (mFeatureFlags.businessCallComposer() && call.getExtras() != null) {
422             Bundle extras = call.getExtras();
423             boolean isBusinessCall =
424                     extras.getBoolean(android.telecom.Call.EXTRA_IS_BUSINESS_CALL, false);
425             paramBuilder.setIsBusinessCall(isBusinessCall);
426             if (isBusinessCall) {
427                 Log.i(TAG, "logging business call");
428                 String assertedDisplayName =
429                         extras.getString(android.telecom.Call.EXTRA_ASSERTED_DISPLAY_NAME, "");
430                 if (assertedDisplayName.length() > MAX_NUMBER_OF_CHARACTERS) {
431                     // avoid throwing an IllegalArgumentException and only log the first 256
432                     // characters of the name.
433                     paramBuilder.setAssertedDisplayName(
434                             assertedDisplayName.substring(0, MAX_NUMBER_OF_CHARACTERS));
435                 } else {
436                     paramBuilder.setAssertedDisplayName(assertedDisplayName);
437                 }
438             }
439         }
440         sendAddCallBroadcast(callLogType, call.getAgeMillis());
441 
442         boolean okayToLog =
443                 okayToLogCall(accountHandle, logNumber, call.isEmergencyCall());
444         if (okayToLog) {
445             AddCallArgs args = new AddCallArgs(mContext, paramBuilder.build(),
446                     logCallCompletedListener, call);
447             Log.addEvent(call, LogUtils.Events.LOG_CALL, "number=" + Log.piiHandle(logNumber)
448                     + ",postDial=" + Log.piiHandle(call.getPostDialDigits()) + ",pres="
449                     + call.getHandlePresentation());
450             logCallAsync(args);
451         } else {
452             Log.addEvent(call, LogUtils.Events.SKIP_CALL_LOG);
453         }
454     }
455 
okayToLogCall(PhoneAccountHandle accountHandle, String number, boolean isEmergency)456     boolean okayToLogCall(PhoneAccountHandle accountHandle, String number, boolean isEmergency) {
457         // On some devices, to avoid accidental redialing of emergency numbers, we *never* log
458         // emergency calls to the Call Log.  (This behavior is set on a per-product basis, based
459         // on carrier requirements.)
460         boolean okToLogEmergencyNumber = false;
461         CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService(
462                 Context.CARRIER_CONFIG_SERVICE);
463         PersistableBundle configBundle = (configManager != null) ? configManager.getConfigForSubId(
464                 mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(accountHandle)) : null;
465         if (configBundle != null) {
466             okToLogEmergencyNumber = configBundle.getBoolean(
467                     CarrierConfigManager.KEY_ALLOW_EMERGENCY_NUMBERS_IN_CALL_LOG_BOOL);
468         }
469 
470         // Don't log emergency numbers if the device doesn't allow it.
471         return (!isEmergency || okToLogEmergencyNumber)
472                 && !isUnloggableNumber(number, configBundle);
473     }
474 
isUnloggableNumber(String callNumber, PersistableBundle carrierConfig)475     private boolean isUnloggableNumber(String callNumber, PersistableBundle carrierConfig) {
476         String normalizedNumber = PhoneNumberUtils.normalizeNumber(callNumber);
477         String[] unloggableNumbersFromCarrierConfig = carrierConfig == null ? null
478                 : carrierConfig.getStringArray(
479                         CarrierConfigManager.KEY_UNLOGGABLE_NUMBERS_STRING_ARRAY);
480         String[] unloggableNumbersFromMccConfig = mContext.getResources()
481                 .getStringArray(com.android.internal.R.array.unloggable_phone_numbers);
482         return Stream.concat(
483                 unloggableNumbersFromCarrierConfig == null ?
484                         Stream.empty() : Arrays.stream(unloggableNumbersFromCarrierConfig),
485                 unloggableNumbersFromMccConfig == null ?
486                         Stream.empty() : Arrays.stream(unloggableNumbersFromMccConfig)
487         ).anyMatch(unloggableNumber -> Objects.equals(unloggableNumber, normalizedNumber));
488     }
489 
490     /**
491      * Based on the video state of the call, determines the call features applicable for the call.
492      *
493      * @param videoState The video state.
494      * @param isPulledCall {@code true} if this call was pulled to another device.
495      * @param isStoreHd {@code true} if this call was used HD.
496      * @param isWifi {@code true} if this call was used wifi.
497      * @param isUsingAssistedDialing {@code true} if this call used assisted dialing.
498      * @return The call features.
499      */
getCallFeatures(int videoState, boolean isPulledCall, boolean isStoreHd, boolean isWifi, boolean isUsingAssistedDialing, boolean isRtt, boolean isVolte)500     private static int getCallFeatures(int videoState, boolean isPulledCall, boolean isStoreHd,
501             boolean isWifi, boolean isUsingAssistedDialing, boolean isRtt, boolean isVolte) {
502         int features = 0;
503         if (VideoProfile.isVideo(videoState)) {
504             features |= Calls.FEATURES_VIDEO;
505         }
506         if (isPulledCall) {
507             features |= Calls.FEATURES_PULLED_EXTERNALLY;
508         }
509         if (isStoreHd) {
510             features |= Calls.FEATURES_HD_CALL;
511         }
512         if (isWifi) {
513             features |= Calls.FEATURES_WIFI;
514         }
515         if (isUsingAssistedDialing) {
516             features |= Calls.FEATURES_ASSISTED_DIALING_USED;
517         }
518         if (isRtt) {
519             features |= Calls.FEATURES_RTT;
520         }
521         if (isVolte) {
522             features |= Calls.FEATURES_VOLTE;
523         }
524         return features;
525     }
526 
527     /**
528      * Retrieve the phone number from the call, and then process it before returning the
529      * actual number that is to be logged.
530      *
531      * @param call The phone connection.
532      * @return the phone number to be logged.
533      */
getLogNumber(Call call)534     private String getLogNumber(Call call) {
535         Uri handle = call.getOriginalHandle();
536 
537         if (handle == null) {
538             return null;
539         }
540 
541         String handleString = handle.getSchemeSpecificPart();
542         if (!PhoneNumberUtils.isUriNumber(handleString)) {
543             handleString = PhoneNumberUtils.stripSeparators(handleString);
544         }
545         return handleString;
546     }
547 
548     /**
549      * Adds the call defined by the parameters in the provided AddCallArgs to the CallLogProvider
550      * using an AsyncTask to avoid blocking the main thread.
551      *
552      * @param args Prepopulated call details.
553      * @return A handle to the AsyncTask that will add the call to the call log asynchronously.
554      */
logCallAsync(AddCallArgs args)555     public AsyncTask<AddCallArgs, Void, Uri[]> logCallAsync(AddCallArgs args) {
556         return new LogCallAsyncTask().execute(args);
557     }
558 
559     /**
560      * Helper AsyncTask to access the call logs database asynchronously since database operations
561      * can take a long time depending on the system's load. Since it extends AsyncTask, it uses
562      * its own thread pool.
563      */
564     private class LogCallAsyncTask extends AsyncTask<AddCallArgs, Void, Uri[]> {
565 
566         private LogCallCompletedListener[] mListeners;
567 
568         @Override
doInBackground(AddCallArgs... callList)569         protected Uri[] doInBackground(AddCallArgs... callList) {
570             int count = callList.length;
571             Uri[] result = new Uri[count];
572             mListeners = new LogCallCompletedListener[count];
573             for (int i = 0; i < count; i++) {
574                 AddCallArgs c = callList[i];
575                 mListeners[i] = c.logCallCompletedListener;
576                 try {
577                     result[i] = Calls.addCall(c.context, c.params);
578                     Log.i(TAG, "LogCall; logged callId=%s, uri=%s",
579                             c.call.getId(), result[i]);
580                     if (result[i] == null) {
581                         // No call was added or even worse we lost a call in the log.  Trigger an
582                         // anomaly report.  Note: it technically possible that an app modified the
583                         // call log while we were writing to it here; that is pretty unlikely, and
584                         // the goal here is to try and identify potential anomalous conditions with
585                         // logging calls.
586                         mAnomalyReporterAdapter.reportAnomaly(LOG_CALL_FAILED_ANOMALY_ID,
587                                 LOG_CALL_FAILED_ANOMALY_DESC);
588                     }
589                 } catch (Exception e) {
590                     // This is very rare but may happen in legitimate cases.
591                     // E.g. If the phone is encrypted and thus write request fails, it may cause
592                     // some kind of Exception (right now it is IllegalArgumentException, but this
593                     // might change).
594                     //
595                     // We don't want to crash the whole process just because of that, so just log
596                     // it instead.
597                     Log.e(TAG, e, "LogCall: Exception raised adding callId=%s", c.call.getId());
598                     result[i] = null;
599                     mAnomalyReporterAdapter.reportAnomaly(LOG_CALL_FAILED_ANOMALY_ID,
600                             LOG_CALL_FAILED_ANOMALY_DESC);
601                 }
602             }
603             return result;
604         }
605 
606         @Override
onPostExecute(Uri[] result)607         protected void onPostExecute(Uri[] result) {
608             for (int i = 0; i < result.length; i++) {
609                 Uri uri = result[i];
610                 /*
611                  Performs a simple correctness check to make sure the call was written in the
612                  database.
613                  Typically there is only one result per call so it is easy to identify which one
614                  failed.
615                  */
616                 if (uri == null) {
617                     Log.w(TAG, "Failed to write call to the log.");
618                 }
619                 if (mListeners[i] != null) {
620                     mListeners[i].onLogCompleted(uri);
621                 }
622             }
623         }
624     }
625 
sendAddCallBroadcast(int callType, long duration)626     private void sendAddCallBroadcast(int callType, long duration) {
627         Intent callAddIntent = new Intent(ACTION_CALLS_TABLE_ADD_ENTRY);
628         callAddIntent.putExtra(CALL_TYPE, callType);
629         callAddIntent.putExtra(CALL_DURATION, duration);
630         mContext.sendBroadcast(callAddIntent, PERMISSION_PROCESS_CALLLOG_INFO);
631     }
632 
getCountryIsoFromCountry(Country country)633     private String getCountryIsoFromCountry(Country country) {
634         if(country == null) {
635             // Fallback to Locale if there are issues with CountryDetector
636             Log.w(TAG, "Value for country was null. Falling back to Locale.");
637             return Locale.getDefault().getCountry();
638         }
639 
640         return country.getCountryCode();
641     }
642 
643     /**
644      * Get the current country code
645      *
646      * @return the ISO 3166-1 two letters country code of current country.
647      */
getCountryIso()648     public String getCountryIso() {
649         synchronized (mLock) {
650             if (mCurrentCountryIso == null) {
651                 // Moving this into the constructor will pose issues if the service is not yet set
652                 // up, causing a RemoteException to be thrown. Note that the callback is only
653                 // registered if the country iso cache is null (so in an ideal setting, this should
654                 // only require a one-time configuration).
655                 final CountryDetector countryDetector =
656                         (CountryDetector) mContext.getSystemService(Context.COUNTRY_DETECTOR);
657                 if (countryDetector != null) {
658                     countryDetector.registerCountryDetectorCallback(
659                             mCountryCodeExecutor, this::countryCodeConsumer);
660                 }
661                 mCurrentCountryIso = getCountryIsoFromCountry(mCurrentCountry);
662             }
663             return mCurrentCountryIso;
664         }
665     }
666 
667     /** Consumer to receive the country code if it changes. */
countryCodeConsumer(Country newCountry)668     private void countryCodeConsumer(Country newCountry) {
669         Log.startSession("CLM.cCC");
670         try {
671             Log.i(TAG, "Country ISO changed. Retrieving new ISO...");
672             synchronized (mLock) {
673                 mCurrentCountry = newCountry;
674                 mCurrentCountryIso = getCountryIsoFromCountry(newCountry);
675             }
676         } finally {
677             Log.endSession();
678         }
679     }
680 
681     @VisibleForTesting
setAnomalyReporterAdapter(AnomalyReporterAdapter anomalyReporterAdapter)682     public void setAnomalyReporterAdapter(AnomalyReporterAdapter anomalyReporterAdapter){
683         mAnomalyReporterAdapter = anomalyReporterAdapter;
684     }
685 }
686