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