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(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     */
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     */
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     */
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     */
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     */
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
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
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         */
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
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
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
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     */
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