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