CallLogManager.java revision 3037ac6f171b6a3627494bb10042ab7adb34366a
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
17package com.android.server.telecom;
18
19import android.annotation.Nullable;
20import android.content.Context;
21import android.content.Intent;
22import android.location.Country;
23import android.location.CountryDetector;
24import android.location.CountryListener;
25import android.net.Uri;
26import android.os.AsyncTask;
27import android.os.Looper;
28import android.os.UserHandle;
29import android.os.PersistableBundle;
30import android.provider.CallLog.Calls;
31import android.telecom.DisconnectCause;
32import android.telecom.PhoneAccount;
33import android.telecom.PhoneAccountHandle;
34import android.telecom.VideoProfile;
35import android.telephony.CarrierConfigManager;
36import android.telephony.PhoneNumberUtils;
37
38// TODO: Needed for move to system service: import com.android.internal.R;
39import com.android.internal.annotations.VisibleForTesting;
40import com.android.internal.telephony.CallerInfo;
41
42import 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
50public final class CallLogManager extends CallsManagerListenerBase {
51
52    public interface LogCallCompletedListener {
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         */
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
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
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
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(
176                                    new MissedCallNotifier.CallInfo(call));
177                        }
178                    });
179        } else {
180            logCall(call, type, null);
181        }
182    }
183
184    /**
185     * Logs a call to the call log based on the {@link Call} object passed in.
186     *
187     * @param call The call object being logged
188     * @param callLogType The type of call log entry to log this call as. See:
189     *     {@link android.provider.CallLog.Calls#INCOMING_TYPE}
190     *     {@link android.provider.CallLog.Calls#OUTGOING_TYPE}
191     *     {@link android.provider.CallLog.Calls#MISSED_TYPE}
192     * @param logCallCompletedListener optional callback called after the call is logged.
193     */
194    void logCall(Call call, int callLogType,
195        @Nullable LogCallCompletedListener logCallCompletedListener) {
196        final long creationTime = call.getCreationTimeMillis();
197        final long age = call.getAgeMillis();
198
199        final String logNumber = getLogNumber(call);
200
201        Log.d(TAG, "logNumber set to: %s", Log.pii(logNumber));
202
203        final PhoneAccountHandle emergencyAccountHandle =
204                TelephonyUtil.getDefaultEmergencyPhoneAccount().getAccountHandle();
205
206        String formattedViaNumber = PhoneNumberUtils.formatNumber(call.getViaNumber(),
207                getCountryIso());
208        formattedViaNumber = (formattedViaNumber != null) ?
209                formattedViaNumber : call.getViaNumber();
210
211        PhoneAccountHandle accountHandle = call.getTargetPhoneAccount();
212        if (emergencyAccountHandle.equals(accountHandle)) {
213            accountHandle = null;
214        }
215
216        Long callDataUsage = call.getCallDataUsage() == Call.DATA_USAGE_NOT_SET ? null :
217                call.getCallDataUsage();
218
219        int callFeatures = getCallFeatures(call.getVideoStateHistory(),
220                call.getDisconnectCause().getCode() == DisconnectCause.CALL_PULLED);
221        logCall(call.getCallerInfo(), logNumber, call.getPostDialDigits(), formattedViaNumber,
222                call.getHandlePresentation(), callLogType, callFeatures, accountHandle,
223                creationTime, age, callDataUsage, call.isEmergencyCall(), call.getInitiatingUser(),
224                logCallCompletedListener);
225    }
226
227    /**
228     * Inserts a call into the call log, based on the parameters passed in.
229     *
230     * @param callerInfo Caller details.
231     * @param number The number the call was made to or from.
232     * @param postDialDigits The post-dial digits that were dialed after the number,
233     *                       if it was an outgoing call. Otherwise ''.
234     * @param presentation
235     * @param callType The type of call.
236     * @param features The features of the call.
237     * @param start The start time of the call, in milliseconds.
238     * @param duration The duration of the call, in milliseconds.
239     * @param dataUsage The data usage for the call, null if not applicable.
240     * @param isEmergency {@code true} if this is an emergency call, {@code false} otherwise.
241     * @param logCallCompletedListener optional callback called after the call is logged.
242     */
243    private void logCall(
244            CallerInfo callerInfo,
245            String number,
246            String postDialDigits,
247            String viaNumber,
248            int presentation,
249            int callType,
250            int features,
251            PhoneAccountHandle accountHandle,
252            long start,
253            long duration,
254            Long dataUsage,
255            boolean isEmergency,
256            UserHandle initiatingUser,
257            @Nullable LogCallCompletedListener logCallCompletedListener) {
258
259        // On some devices, to avoid accidental redialing of emergency numbers, we *never* log
260        // emergency calls to the Call Log.  (This behavior is set on a per-product basis, based
261        // on carrier requirements.)
262        boolean okToLogEmergencyNumber = false;
263        CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService(
264                Context.CARRIER_CONFIG_SERVICE);
265        PersistableBundle configBundle = configManager.getConfig();
266        if (configBundle != null) {
267            okToLogEmergencyNumber = configBundle.getBoolean(
268                    CarrierConfigManager.KEY_ALLOW_EMERGENCY_NUMBERS_IN_CALL_LOG_BOOL);
269        }
270
271        // Don't log emergency numbers if the device doesn't allow it.
272        final boolean isOkToLogThisCall = !isEmergency || okToLogEmergencyNumber;
273
274        sendAddCallBroadcast(callType, duration);
275
276        if (isOkToLogThisCall) {
277            Log.d(TAG, "Logging Calllog entry: " + callerInfo + ", "
278                    + Log.pii(number) + "," + presentation + ", " + callType
279                    + ", " + start + ", " + duration);
280            AddCallArgs args = new AddCallArgs(mContext, callerInfo, number, postDialDigits,
281                    viaNumber, presentation, callType, features, accountHandle, start, duration,
282                    dataUsage, initiatingUser, logCallCompletedListener);
283            logCallAsync(args);
284        } else {
285          Log.d(TAG, "Not adding emergency call to call log.");
286        }
287    }
288
289    /**
290     * Based on the video state of the call, determines the call features applicable for the call.
291     *
292     * @param videoState The video state.
293     * @param isPulledCall {@code true} if this call was pulled to another device.
294     * @return The call features.
295     */
296    private static int getCallFeatures(int videoState, boolean isPulledCall) {
297        int features = 0;
298        if (VideoProfile.isVideo(videoState)) {
299            features |= Calls.FEATURES_VIDEO;
300        }
301        if (isPulledCall) {
302            features |= Calls.FEATURES_PULLED_EXTERNALLY;
303        }
304        return features;
305    }
306
307    /**
308     * Retrieve the phone number from the call, and then process it before returning the
309     * actual number that is to be logged.
310     *
311     * @param call The phone connection.
312     * @return the phone number to be logged.
313     */
314    private String getLogNumber(Call call) {
315        Uri handle = call.getOriginalHandle();
316
317        if (handle == null) {
318            return null;
319        }
320
321        String handleString = handle.getSchemeSpecificPart();
322        if (!PhoneNumberUtils.isUriNumber(handleString)) {
323            handleString = PhoneNumberUtils.stripSeparators(handleString);
324        }
325        return handleString;
326    }
327
328    /**
329     * Adds the call defined by the parameters in the provided AddCallArgs to the CallLogProvider
330     * using an AsyncTask to avoid blocking the main thread.
331     *
332     * @param args Prepopulated call details.
333     * @return A handle to the AsyncTask that will add the call to the call log asynchronously.
334     */
335    public AsyncTask<AddCallArgs, Void, Uri[]> logCallAsync(AddCallArgs args) {
336        return new LogCallAsyncTask().execute(args);
337    }
338
339    /**
340     * Helper AsyncTask to access the call logs database asynchronously since database operations
341     * can take a long time depending on the system's load. Since it extends AsyncTask, it uses
342     * its own thread pool.
343     */
344    private class LogCallAsyncTask extends AsyncTask<AddCallArgs, Void, Uri[]> {
345
346        private LogCallCompletedListener[] mListeners;
347
348        @Override
349        protected Uri[] doInBackground(AddCallArgs... callList) {
350            int count = callList.length;
351            Uri[] result = new Uri[count];
352            mListeners = new LogCallCompletedListener[count];
353            for (int i = 0; i < count; i++) {
354                AddCallArgs c = callList[i];
355                mListeners[i] = c.logCallCompletedListener;
356                try {
357                    // May block.
358                    result[i] = addCall(c);
359                } catch (Exception e) {
360                    // This is very rare but may happen in legitimate cases.
361                    // E.g. If the phone is encrypted and thus write request fails, it may cause
362                    // some kind of Exception (right now it is IllegalArgumentException, but this
363                    // might change).
364                    //
365                    // We don't want to crash the whole process just because of that, so just log
366                    // it instead.
367                    Log.e(TAG, e, "Exception raised during adding CallLog entry.");
368                    result[i] = null;
369                }
370            }
371            return result;
372        }
373
374        private Uri addCall(AddCallArgs c) {
375            PhoneAccount phoneAccount = mPhoneAccountRegistrar
376                    .getPhoneAccountUnchecked(c.accountHandle);
377            if (phoneAccount != null &&
378                    phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) {
379                if (c.initiatingUser != null &&
380                        UserUtil.isManagedProfile(mContext, c.initiatingUser)) {
381                    return addCall(c, c.initiatingUser);
382                } else {
383                    return addCall(c, null);
384                }
385            } else {
386                return addCall(c, c.accountHandle == null ? null : c.accountHandle.getUserHandle());
387            }
388        }
389
390        /**
391         * Insert the call to a specific user or all users except managed profile.
392         * @param c context
393         * @param userToBeInserted user handle of user that the call going be inserted to. null
394         *                         if insert to all users except managed profile.
395         */
396        private Uri addCall(AddCallArgs c, UserHandle userToBeInserted) {
397            return Calls.addCall(c.callerInfo, c.context, c.number, c.postDialDigits, c.viaNumber,
398                    c.presentation, c.callType, c.features, c.accountHandle, c.timestamp,
399                    c.durationInSec, c.dataUsage, userToBeInserted == null,
400                    userToBeInserted);
401        }
402
403
404        @Override
405        protected void onPostExecute(Uri[] result) {
406            for (int i = 0; i < result.length; i++) {
407                Uri uri = result[i];
408                /*
409                 Performs a simple sanity check to make sure the call was written in the database.
410                 Typically there is only one result per call so it is easy to identify which one
411                 failed.
412                 */
413                if (uri == null) {
414                    Log.w(TAG, "Failed to write call to the log.");
415                }
416                if (mListeners[i] != null) {
417                    mListeners[i].onLogCompleted(uri);
418                }
419            }
420        }
421    }
422
423    private void sendAddCallBroadcast(int callType, long duration) {
424        Intent callAddIntent = new Intent(ACTION_CALLS_TABLE_ADD_ENTRY);
425        callAddIntent.putExtra(CALL_TYPE, callType);
426        callAddIntent.putExtra(CALL_DURATION, duration);
427        mContext.sendBroadcast(callAddIntent, PERMISSION_PROCESS_CALLLOG_INFO);
428    }
429
430    private String getCountryIsoFromCountry(Country country) {
431        if(country == null) {
432            // Fallback to Locale if there are issues with CountryDetector
433            Log.w(TAG, "Value for country was null. Falling back to Locale.");
434            return Locale.getDefault().getCountry();
435        }
436
437        return country.getCountryIso();
438    }
439
440    /**
441     * Get the current country code
442     *
443     * @return the ISO 3166-1 two letters country code of current country.
444     */
445    public String getCountryIso() {
446        synchronized (mLock) {
447            if (mCurrentCountryIso == null) {
448                Log.i(TAG, "Country cache is null. Detecting Country and Setting Cache...");
449                final CountryDetector countryDetector =
450                        (CountryDetector) mContext.getSystemService(Context.COUNTRY_DETECTOR);
451                Country country = null;
452                if (countryDetector != null) {
453                    country = countryDetector.detectCountry();
454
455                    countryDetector.addCountryListener((newCountry) -> {
456                        Log.startSession("CLM.oCD");
457                        try {
458                            synchronized (mLock) {
459                                Log.i(TAG, "Country ISO changed. Retrieving new ISO...");
460                                mCurrentCountryIso = getCountryIsoFromCountry(newCountry);
461                            }
462                        } finally {
463                            Log.endSession();
464                        }
465                    }, Looper.getMainLooper());
466                }
467                mCurrentCountryIso = getCountryIsoFromCountry(country);
468            }
469            return mCurrentCountryIso;
470        }
471    }
472}
473