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