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