• 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 android.annotation.Nullable;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.location.Country;
23 import android.location.CountryDetector;
24 import android.net.Uri;
25 import android.os.AsyncTask;
26 import android.os.Looper;
27 import android.os.UserHandle;
28 import android.os.PersistableBundle;
29 import android.provider.CallLog.Calls;
30 import android.telecom.Connection;
31 import android.telecom.DisconnectCause;
32 import android.telecom.Log;
33 import android.telecom.PhoneAccount;
34 import android.telecom.PhoneAccountHandle;
35 import android.telecom.VideoProfile;
36 import android.telephony.CarrierConfigManager;
37 import android.telephony.PhoneNumberUtils;
38 
39 // TODO: Needed for move to system service: import com.android.internal.R;
40 import com.android.internal.annotations.VisibleForTesting;
41 import com.android.internal.telephony.CallerInfo;
42 
43 import java.util.Locale;
44 
45 /**
46  * Helper class that provides functionality to write information about calls and their associated
47  * caller details to the call log. All logging activity will be performed asynchronously in a
48  * background thread to avoid blocking on the main thread.
49  */
50 @VisibleForTesting
51 public final class CallLogManager extends CallsManagerListenerBase {
52 
53     public interface LogCallCompletedListener {
onLogCompleted(@ullable Uri uri)54         void onLogCompleted(@Nullable Uri uri);
55     }
56 
57     /**
58      * Parameter object to hold the arguments to add a call in the call log DB.
59      */
60     private static class AddCallArgs {
61         /**
62          * @param callerInfo Caller details.
63          * @param number The phone number to be logged.
64          * @param presentation Number presentation of the phone number to be logged.
65          * @param callType The type of call (e.g INCOMING_TYPE). @see
66          *     {@link android.provider.CallLog} for the list of values.
67          * @param features The features of the call (e.g. FEATURES_VIDEO). @see
68          *     {@link android.provider.CallLog} for the list of values.
69          * @param creationDate Time when the call was created (milliseconds since epoch).
70          * @param durationInMillis Duration of the call (milliseconds).
71          * @param dataUsage Data usage in bytes, or null if not applicable.
72          * @param isRead Indicates if the entry has been read or not.
73          * @param logCallCompletedListener optional callback called after the call is logged.
74          */
AddCallArgs(Context context, CallerInfo callerInfo, String number, String postDialDigits, String viaNumber, int presentation, int callType, int features, PhoneAccountHandle accountHandle, long creationDate, long durationInMillis, Long dataUsage, UserHandle initiatingUser, boolean isRead, @Nullable LogCallCompletedListener logCallCompletedListener)75         public AddCallArgs(Context context, CallerInfo callerInfo, String number,
76                 String postDialDigits, String viaNumber, int presentation, int callType,
77                 int features, PhoneAccountHandle accountHandle, long creationDate,
78                 long durationInMillis, Long dataUsage, UserHandle initiatingUser, boolean isRead,
79                 @Nullable LogCallCompletedListener logCallCompletedListener) {
80             this.context = context;
81             this.callerInfo = callerInfo;
82             this.number = number;
83             this.postDialDigits = postDialDigits;
84             this.viaNumber = viaNumber;
85             this.presentation = presentation;
86             this.callType = callType;
87             this.features = features;
88             this.accountHandle = accountHandle;
89             this.timestamp = creationDate;
90             this.durationInSec = (int)(durationInMillis / 1000);
91             this.dataUsage = dataUsage;
92             this.initiatingUser = initiatingUser;
93             this.isRead = isRead;
94             this.logCallCompletedListener = logCallCompletedListener;
95         }
96         // Since the members are accessed directly, we don't use the
97         // mXxxx notation.
98         public final Context context;
99         public final CallerInfo callerInfo;
100         public final String number;
101         public final String postDialDigits;
102         public final String viaNumber;
103         public final int presentation;
104         public final int callType;
105         public final int features;
106         public final PhoneAccountHandle accountHandle;
107         public final long timestamp;
108         public final int durationInSec;
109         public final Long dataUsage;
110         public final UserHandle initiatingUser;
111         public final boolean isRead;
112 
113         @Nullable
114         public final LogCallCompletedListener logCallCompletedListener;
115     }
116 
117     private static final String TAG = CallLogManager.class.getSimpleName();
118 
119     private final Context mContext;
120     private final PhoneAccountRegistrar mPhoneAccountRegistrar;
121     private final MissedCallNotifier mMissedCallNotifier;
122     private static final String ACTION_CALLS_TABLE_ADD_ENTRY =
123                 "com.android.server.telecom.intent.action.CALLS_ADD_ENTRY";
124     private static final String PERMISSION_PROCESS_CALLLOG_INFO =
125                 "android.permission.PROCESS_CALLLOG_INFO";
126     private static final String CALL_TYPE = "callType";
127     private static final String CALL_DURATION = "duration";
128 
129     private Object mLock;
130     private String mCurrentCountryIso;
131 
CallLogManager(Context context, PhoneAccountRegistrar phoneAccountRegistrar, MissedCallNotifier missedCallNotifier)132     public CallLogManager(Context context, PhoneAccountRegistrar phoneAccountRegistrar,
133             MissedCallNotifier missedCallNotifier) {
134         mContext = context;
135         mPhoneAccountRegistrar = phoneAccountRegistrar;
136         mMissedCallNotifier = missedCallNotifier;
137         mLock = new Object();
138     }
139 
140     @Override
onCallStateChanged(Call call, int oldState, int newState)141     public void onCallStateChanged(Call call, int oldState, int newState) {
142         int disconnectCause = call.getDisconnectCause().getCode();
143         boolean isNewlyDisconnected =
144                 newState == CallState.DISCONNECTED || newState == CallState.ABORTED;
145         boolean isCallCanceled = isNewlyDisconnected && disconnectCause == DisconnectCause.CANCELED;
146 
147         // Log newly disconnected calls only if:
148         // 1) It was not in the "choose account" phase when disconnected
149         // 2) It is a conference call
150         // 3) Call was not explicitly canceled
151         // 4) Call is not an external call
152         // 5) Call is not a self-managed call OR call is a self-managed call which has indicated it
153         //    should be logged in its PhoneAccount
154         if (isNewlyDisconnected &&
155                 (oldState != CallState.SELECT_PHONE_ACCOUNT &&
156                         !call.isConference() &&
157                         !isCallCanceled) &&
158                 !call.isExternalCall() &&
159                 (!call.isSelfManaged() ||
160                         (call.isLoggedSelfManaged() &&
161                                 (call.getHandoverState() == HandoverState.HANDOVER_NONE ||
162                                 call.getHandoverState() == HandoverState.HANDOVER_COMPLETE)))) {
163             int type;
164             if (!call.isIncoming()) {
165                 type = Calls.OUTGOING_TYPE;
166             } else if (disconnectCause == DisconnectCause.MISSED) {
167                 type = Calls.MISSED_TYPE;
168             } else if (disconnectCause == DisconnectCause.ANSWERED_ELSEWHERE) {
169                 type = Calls.ANSWERED_EXTERNALLY_TYPE;
170             } else if (disconnectCause == DisconnectCause.REJECTED) {
171                 type = Calls.REJECTED_TYPE;
172             } else {
173                 type = Calls.INCOMING_TYPE;
174             }
175             // Always show the notification for managed calls. For self-managed calls, it is up to
176             // the app to show the notification, so suppress the notification when logging the call.
177             boolean showNotification = !call.isSelfManaged();
178             logCall(call, type, showNotification);
179         }
180     }
181 
logCall(Call call, int type, boolean showNotificationForMissedCall)182     void logCall(Call call, int type, boolean showNotificationForMissedCall) {
183         if (type == Calls.MISSED_TYPE && showNotificationForMissedCall) {
184             logCall(call, Calls.MISSED_TYPE,
185                     new LogCallCompletedListener() {
186                         @Override
187                         public void onLogCompleted(@Nullable Uri uri) {
188                             mMissedCallNotifier.showMissedCallNotification(
189                                     new MissedCallNotifier.CallInfo(call));
190                         }
191                     });
192         } else {
193             logCall(call, type, null);
194         }
195     }
196 
197     /**
198      * Logs a call to the call log based on the {@link Call} object passed in.
199      *
200      * @param call The call object being logged
201      * @param callLogType The type of call log entry to log this call as. See:
202      *     {@link android.provider.CallLog.Calls#INCOMING_TYPE}
203      *     {@link android.provider.CallLog.Calls#OUTGOING_TYPE}
204      *     {@link android.provider.CallLog.Calls#MISSED_TYPE}
205      * @param logCallCompletedListener optional callback called after the call is logged.
206      */
logCall(Call call, int callLogType, @Nullable LogCallCompletedListener logCallCompletedListener)207     void logCall(Call call, int callLogType,
208         @Nullable LogCallCompletedListener logCallCompletedListener) {
209         final long creationTime = call.getCreationTimeMillis();
210         final long age = call.getAgeMillis();
211 
212         final String logNumber = getLogNumber(call);
213 
214         Log.d(TAG, "logNumber set to: %s", Log.pii(logNumber));
215 
216         final PhoneAccountHandle emergencyAccountHandle =
217                 TelephonyUtil.getDefaultEmergencyPhoneAccount().getAccountHandle();
218 
219         String formattedViaNumber = PhoneNumberUtils.formatNumber(call.getViaNumber(),
220                 getCountryIso());
221         formattedViaNumber = (formattedViaNumber != null) ?
222                 formattedViaNumber : call.getViaNumber();
223 
224         PhoneAccountHandle accountHandle = call.getTargetPhoneAccount();
225         if (emergencyAccountHandle.equals(accountHandle)) {
226             accountHandle = null;
227         }
228 
229         Long callDataUsage = call.getCallDataUsage() == Call.DATA_USAGE_NOT_SET ? null :
230                 call.getCallDataUsage();
231 
232         int callFeatures = getCallFeatures(call.getVideoStateHistory(),
233                 call.getDisconnectCause().getCode() == DisconnectCause.CALL_PULLED,
234                 shouldSaveHdInfo(call, accountHandle),
235                 (call.getConnectionProperties() & Connection.PROPERTY_ASSISTED_DIALING_USED) ==
236                         Connection.PROPERTY_ASSISTED_DIALING_USED,
237                 call.wasEverRttCall());
238         logCall(call.getCallerInfo(), logNumber, call.getPostDialDigits(), formattedViaNumber,
239                 call.getHandlePresentation(), callLogType, callFeatures, accountHandle,
240                 creationTime, age, callDataUsage, call.isEmergencyCall(), call.getInitiatingUser(),
241                 call.isSelfManaged(), logCallCompletedListener);
242     }
243 
244     /**
245      * Inserts a call into the call log, based on the parameters passed in.
246      *
247      * @param callerInfo Caller details.
248      * @param number The number the call was made to or from.
249      * @param postDialDigits The post-dial digits that were dialed after the number,
250      *                       if it was an outgoing call. Otherwise ''.
251      * @param presentation
252      * @param callType The type of call.
253      * @param features The features of the call.
254      * @param start The start time of the call, in milliseconds.
255      * @param duration The duration of the call, in milliseconds.
256      * @param dataUsage The data usage for the call, null if not applicable.
257      * @param isEmergency {@code true} if this is an emergency call, {@code false} otherwise.
258      * @param logCallCompletedListener optional callback called after the call is logged.
259      * @param initiatingUser The user the call was initiated under.
260      * @param isSelfManaged {@code true} if this is a self-managed call, {@code false} otherwise.
261      */
logCall( CallerInfo callerInfo, String number, String postDialDigits, String viaNumber, int presentation, int callType, int features, PhoneAccountHandle accountHandle, long start, long duration, Long dataUsage, boolean isEmergency, UserHandle initiatingUser, boolean isSelfManaged, @Nullable LogCallCompletedListener logCallCompletedListener)262     private void logCall(
263             CallerInfo callerInfo,
264             String number,
265             String postDialDigits,
266             String viaNumber,
267             int presentation,
268             int callType,
269             int features,
270             PhoneAccountHandle accountHandle,
271             long start,
272             long duration,
273             Long dataUsage,
274             boolean isEmergency,
275             UserHandle initiatingUser,
276             boolean isSelfManaged,
277             @Nullable LogCallCompletedListener logCallCompletedListener) {
278 
279         // On some devices, to avoid accidental redialing of emergency numbers, we *never* log
280         // emergency calls to the Call Log.  (This behavior is set on a per-product basis, based
281         // on carrier requirements.)
282         boolean okToLogEmergencyNumber = false;
283         CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService(
284                 Context.CARRIER_CONFIG_SERVICE);
285         PersistableBundle configBundle = configManager.getConfigForSubId(
286                 mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(accountHandle));
287         if (configBundle != null) {
288             okToLogEmergencyNumber = configBundle.getBoolean(
289                     CarrierConfigManager.KEY_ALLOW_EMERGENCY_NUMBERS_IN_CALL_LOG_BOOL);
290         }
291 
292         // Don't log emergency numbers if the device doesn't allow it.
293         final boolean isOkToLogThisCall = !isEmergency || okToLogEmergencyNumber;
294 
295         sendAddCallBroadcast(callType, duration);
296 
297         if (isOkToLogThisCall) {
298             Log.d(TAG, "Logging Call log entry: " + callerInfo + ", "
299                     + Log.pii(number) + "," + presentation + ", " + callType
300                     + ", " + start + ", " + duration);
301             boolean isRead = false;
302             if (isSelfManaged) {
303                 // Mark self-managed calls are read since they're being handled by their own app.
304                 // Their inclusion in the call log is informational only.
305                 isRead = true;
306             }
307             AddCallArgs args = new AddCallArgs(mContext, callerInfo, number, postDialDigits,
308                     viaNumber, presentation, callType, features, accountHandle, start, duration,
309                     dataUsage, initiatingUser, isRead, logCallCompletedListener);
310             logCallAsync(args);
311         } else {
312           Log.d(TAG, "Not adding emergency call to call log.");
313         }
314     }
315 
316     /**
317      * Based on the video state of the call, determines the call features applicable for the call.
318      *
319      * @param videoState The video state.
320      * @param isPulledCall {@code true} if this call was pulled to another device.
321      * @param isStoreHd {@code true} if this call was used HD.
322      * @param isUsingAssistedDialing {@code true} if this call used assisted dialing.
323      * @return The call features.
324      */
getCallFeatures(int videoState, boolean isPulledCall, boolean isStoreHd, boolean isUsingAssistedDialing, boolean isRtt)325     private static int getCallFeatures(int videoState, boolean isPulledCall, boolean isStoreHd,
326             boolean isUsingAssistedDialing, boolean isRtt) {
327         int features = 0;
328         if (VideoProfile.isVideo(videoState)) {
329             features |= Calls.FEATURES_VIDEO;
330         }
331         if (isPulledCall) {
332             features |= Calls.FEATURES_PULLED_EXTERNALLY;
333         }
334         if (isStoreHd) {
335             features |= Calls.FEATURES_HD_CALL;
336         }
337         if (isUsingAssistedDialing) {
338             features |= Calls.FEATURES_ASSISTED_DIALING_USED;
339         }
340         if (isRtt) {
341             features |= Calls.FEATURES_RTT;
342         }
343         return features;
344     }
345 
shouldSaveHdInfo(Call call, PhoneAccountHandle accountHandle)346     private boolean shouldSaveHdInfo(Call call, PhoneAccountHandle accountHandle) {
347         CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService(
348                 Context.CARRIER_CONFIG_SERVICE);
349         PersistableBundle configBundle = null;
350         if (configManager != null) {
351             configBundle = configManager.getConfigForSubId(
352                     mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(accountHandle));
353         }
354         if (configBundle != null && configBundle.getBoolean(
355                 CarrierConfigManager.KEY_IDENTIFY_HIGH_DEFINITION_CALLS_IN_CALL_LOG_BOOL)
356                 && call.wasHighDefAudio()) {
357             return true;
358         }
359         return false;
360     }
361 
362     /**
363      * Retrieve the phone number from the call, and then process it before returning the
364      * actual number that is to be logged.
365      *
366      * @param call The phone connection.
367      * @return the phone number to be logged.
368      */
getLogNumber(Call call)369     private String getLogNumber(Call call) {
370         Uri handle = call.getOriginalHandle();
371 
372         if (handle == null) {
373             return null;
374         }
375 
376         String handleString = handle.getSchemeSpecificPart();
377         if (!PhoneNumberUtils.isUriNumber(handleString)) {
378             handleString = PhoneNumberUtils.stripSeparators(handleString);
379         }
380         return handleString;
381     }
382 
383     /**
384      * Adds the call defined by the parameters in the provided AddCallArgs to the CallLogProvider
385      * using an AsyncTask to avoid blocking the main thread.
386      *
387      * @param args Prepopulated call details.
388      * @return A handle to the AsyncTask that will add the call to the call log asynchronously.
389      */
logCallAsync(AddCallArgs args)390     public AsyncTask<AddCallArgs, Void, Uri[]> logCallAsync(AddCallArgs args) {
391         return new LogCallAsyncTask().execute(args);
392     }
393 
394     /**
395      * Helper AsyncTask to access the call logs database asynchronously since database operations
396      * can take a long time depending on the system's load. Since it extends AsyncTask, it uses
397      * its own thread pool.
398      */
399     private class LogCallAsyncTask extends AsyncTask<AddCallArgs, Void, Uri[]> {
400 
401         private LogCallCompletedListener[] mListeners;
402 
403         @Override
doInBackground(AddCallArgs... callList)404         protected Uri[] doInBackground(AddCallArgs... callList) {
405             int count = callList.length;
406             Uri[] result = new Uri[count];
407             mListeners = new LogCallCompletedListener[count];
408             for (int i = 0; i < count; i++) {
409                 AddCallArgs c = callList[i];
410                 mListeners[i] = c.logCallCompletedListener;
411                 try {
412                     // May block.
413                     result[i] = addCall(c);
414                 } catch (Exception e) {
415                     // This is very rare but may happen in legitimate cases.
416                     // E.g. If the phone is encrypted and thus write request fails, it may cause
417                     // some kind of Exception (right now it is IllegalArgumentException, but this
418                     // might change).
419                     //
420                     // We don't want to crash the whole process just because of that, so just log
421                     // it instead.
422                     Log.e(TAG, e, "Exception raised during adding CallLog entry.");
423                     result[i] = null;
424                 }
425             }
426             return result;
427         }
428 
addCall(AddCallArgs c)429         private Uri addCall(AddCallArgs c) {
430             PhoneAccount phoneAccount = mPhoneAccountRegistrar
431                     .getPhoneAccountUnchecked(c.accountHandle);
432             if (phoneAccount != null &&
433                     phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) {
434                 if (c.initiatingUser != null &&
435                         UserUtil.isManagedProfile(mContext, c.initiatingUser)) {
436                     return addCall(c, c.initiatingUser);
437                 } else {
438                     return addCall(c, null);
439                 }
440             } else {
441                 return addCall(c, c.accountHandle == null ? null : c.accountHandle.getUserHandle());
442             }
443         }
444 
445         /**
446          * Insert the call to a specific user or all users except managed profile.
447          * @param c context
448          * @param userToBeInserted user handle of user that the call going be inserted to. null
449          *                         if insert to all users except managed profile.
450          */
addCall(AddCallArgs c, UserHandle userToBeInserted)451         private Uri addCall(AddCallArgs c, UserHandle userToBeInserted) {
452             return Calls.addCall(c.callerInfo, c.context, c.number, c.postDialDigits, c.viaNumber,
453                     c.presentation, c.callType, c.features, c.accountHandle, c.timestamp,
454                     c.durationInSec, c.dataUsage, userToBeInserted == null,
455                     userToBeInserted, c.isRead);
456         }
457 
458 
459         @Override
onPostExecute(Uri[] result)460         protected void onPostExecute(Uri[] result) {
461             for (int i = 0; i < result.length; i++) {
462                 Uri uri = result[i];
463                 /*
464                  Performs a simple sanity check to make sure the call was written in the database.
465                  Typically there is only one result per call so it is easy to identify which one
466                  failed.
467                  */
468                 if (uri == null) {
469                     Log.w(TAG, "Failed to write call to the log.");
470                 }
471                 if (mListeners[i] != null) {
472                     mListeners[i].onLogCompleted(uri);
473                 }
474             }
475         }
476     }
477 
sendAddCallBroadcast(int callType, long duration)478     private void sendAddCallBroadcast(int callType, long duration) {
479         Intent callAddIntent = new Intent(ACTION_CALLS_TABLE_ADD_ENTRY);
480         callAddIntent.putExtra(CALL_TYPE, callType);
481         callAddIntent.putExtra(CALL_DURATION, duration);
482         mContext.sendBroadcast(callAddIntent, PERMISSION_PROCESS_CALLLOG_INFO);
483     }
484 
getCountryIsoFromCountry(Country country)485     private String getCountryIsoFromCountry(Country country) {
486         if(country == null) {
487             // Fallback to Locale if there are issues with CountryDetector
488             Log.w(TAG, "Value for country was null. Falling back to Locale.");
489             return Locale.getDefault().getCountry();
490         }
491 
492         return country.getCountryIso();
493     }
494 
495     /**
496      * Get the current country code
497      *
498      * @return the ISO 3166-1 two letters country code of current country.
499      */
getCountryIso()500     public String getCountryIso() {
501         synchronized (mLock) {
502             if (mCurrentCountryIso == null) {
503                 Log.i(TAG, "Country cache is null. Detecting Country and Setting Cache...");
504                 final CountryDetector countryDetector =
505                         (CountryDetector) mContext.getSystemService(Context.COUNTRY_DETECTOR);
506                 Country country = null;
507                 if (countryDetector != null) {
508                     country = countryDetector.detectCountry();
509 
510                     countryDetector.addCountryListener((newCountry) -> {
511                         Log.startSession("CLM.oCD");
512                         try {
513                             synchronized (mLock) {
514                                 Log.i(TAG, "Country ISO changed. Retrieving new ISO...");
515                                 mCurrentCountryIso = getCountryIsoFromCountry(newCountry);
516                             }
517                         } finally {
518                             Log.endSession();
519                         }
520                     }, Looper.getMainLooper());
521                 }
522                 mCurrentCountryIso = getCountryIsoFromCountry(country);
523             }
524             return mCurrentCountryIso;
525         }
526     }
527 }
528