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