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