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