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