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