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.content.Context;
     20 import android.net.Uri;
     21 import android.os.Handler;
     22 import android.os.Looper;
     23 import android.os.AsyncTask;
     24 import android.telecom.PhoneAccount;
     25 import android.telecom.TimedEvent;
     26 import android.telephony.PhoneNumberUtils;
     27 import android.text.TextUtils;
     28 import android.util.Base64;
     29 
     30 import com.android.internal.annotations.VisibleForTesting;
     31 import com.android.internal.util.IndentingPrintWriter;
     32 
     33 import java.nio.ByteBuffer;
     34 import java.security.MessageDigest;
     35 import java.security.NoSuchAlgorithmException;
     36 import java.text.DateFormat;
     37 import java.text.SimpleDateFormat;
     38 import java.util.ArrayList;
     39 import java.util.Arrays;
     40 import java.util.Collections;
     41 import java.util.Date;
     42 import java.util.HashMap;
     43 import java.util.IllegalFormatException;
     44 import java.util.Iterator;
     45 import java.util.LinkedList;
     46 import java.util.List;
     47 import java.util.Locale;
     48 import java.util.Map;
     49 import java.util.concurrent.ConcurrentHashMap;
     50 import java.util.concurrent.LinkedBlockingQueue;
     51 
     52 /**
     53  * Manages logging for the entire module.
     54  */
     55 @VisibleForTesting
     56 public class Log {
     57 
     58     public static final class Sessions {
     59         public static final String ICA_ANSWER_CALL = "ICA.aC";
     60         public static final String ICA_REJECT_CALL = "ICA.rC";
     61         public static final String ICA_DISCONNECT_CALL = "ICA.dC";
     62         public static final String ICA_HOLD_CALL = "ICA.hC";
     63         public static final String ICA_UNHOLD_CALL = "ICA.uC";
     64         public static final String ICA_MUTE = "ICA.m";
     65         public static final String ICA_SET_AUDIO_ROUTE = "ICA.sAR";
     66         public static final String ICA_CONFERENCE = "ICA.c";
     67         public static final String CSW_HANDLE_CREATE_CONNECTION_COMPLETE = "CSW.hCCC";
     68         public static final String CSW_SET_ACTIVE = "CSW.sA";
     69         public static final String CSW_SET_RINGING = "CSW.sR";
     70         public static final String CSW_SET_DIALING = "CSW.sD";
     71         public static final String CSW_SET_PULLING = "CSW.sP";
     72         public static final String CSW_SET_DISCONNECTED = "CSW.sDc";
     73         public static final String CSW_SET_ON_HOLD = "CSW.sOH";
     74         public static final String CSW_REMOVE_CALL = "CSW.rC";
     75         public static final String CSW_SET_IS_CONFERENCED = "CSW.sIC";
     76         public static final String CSW_ADD_CONFERENCE_CALL = "CSW.aCC";
     77     }
     78 
     79     /**
     80      * Stores the various events associated with {@link Call}s. Also stores all request-response
     81      * pairs amongst the events.
     82      */
     83     public final static class Events {
     84         public static class TimedEventPair {
     85             private static final long DEFAULT_TIMEOUT = 3000L;
     86 
     87             String mRequest;
     88             String mResponse;
     89             String mName;
     90             long mTimeoutMillis = DEFAULT_TIMEOUT;
     91 
     92             public TimedEventPair(String request, String response,
     93                     String name) {
     94                 this.mRequest = request;
     95                 this.mResponse = response;
     96                 this.mName = name;
     97             }
     98 
     99             public TimedEventPair(String request, String response,
    100                     String name, long timeoutMillis) {
    101                 this.mRequest = request;
    102                 this.mResponse = response;
    103                 this.mName = name;
    104                 this.mTimeoutMillis = timeoutMillis;
    105             }
    106         }
    107 
    108         public static final String CREATED = "CREATED";
    109         public static final String DESTROYED = "DESTROYED";
    110         public static final String SET_CONNECTING = "SET_CONNECTING";
    111         public static final String SET_DIALING = "SET_DIALING";
    112         public static final String SET_PULLING = "SET_PULLING";
    113         public static final String SET_ACTIVE = "SET_ACTIVE";
    114         public static final String SET_HOLD = "SET_HOLD";
    115         public static final String SET_RINGING = "SET_RINGING";
    116         public static final String SET_DISCONNECTED = "SET_DISCONNECTED";
    117         public static final String SET_DISCONNECTING = "SET_DISCONNECTING";
    118         public static final String SET_SELECT_PHONE_ACCOUNT = "SET_SELECT_PHONE_ACCOUNT";
    119         public static final String REQUEST_HOLD = "REQUEST_HOLD";
    120         public static final String REQUEST_UNHOLD = "REQUEST_UNHOLD";
    121         public static final String REQUEST_DISCONNECT = "REQUEST_DISCONNECT";
    122         public static final String REQUEST_ACCEPT = "REQUEST_ACCEPT";
    123         public static final String REQUEST_REJECT = "REQUEST_REJECT";
    124         public static final String START_DTMF = "START_DTMF";
    125         public static final String STOP_DTMF = "STOP_DTMF";
    126         public static final String START_RINGER = "START_RINGER";
    127         public static final String STOP_RINGER = "STOP_RINGER";
    128         public static final String SKIP_RINGING = "SKIP_RINGING";
    129         public static final String START_CALL_WAITING_TONE = "START_CALL_WAITING_TONE";
    130         public static final String STOP_CALL_WAITING_TONE = "STOP_CALL_WAITING_TONE";
    131         public static final String START_CONNECTION = "START_CONNECTION";
    132         public static final String BIND_CS = "BIND_CS";
    133         public static final String CS_BOUND = "CS_BOUND";
    134         public static final String CONFERENCE_WITH = "CONF_WITH";
    135         public static final String SPLIT_FROM_CONFERENCE = "CONF_SPLIT";
    136         public static final String SWAP = "SWAP";
    137         public static final String ADD_CHILD = "ADD_CHILD";
    138         public static final String REMOVE_CHILD = "REMOVE_CHILD";
    139         public static final String SET_PARENT = "SET_PARENT";
    140         public static final String MUTE = "MUTE";
    141         public static final String UNMUTE = "UNMUTE";
    142         public static final String AUDIO_ROUTE = "AUDIO_ROUTE";
    143         public static final String AUDIO_ROUTE_EARPIECE = "AUDIO_ROUTE_EARPIECE";
    144         public static final String AUDIO_ROUTE_HEADSET = "AUDIO_ROUTE_HEADSET";
    145         public static final String AUDIO_ROUTE_BT = "AUDIO_ROUTE_BT";
    146         public static final String AUDIO_ROUTE_SPEAKER = "AUDIO_ROUTE_SPEAKER";
    147         public static final String ERROR_LOG = "ERROR";
    148         public static final String USER_LOG_MARK = "USER_LOG_MARK";
    149         public static final String SILENCE = "SILENCE";
    150         public static final String BIND_SCREENING = "BIND_SCREENING";
    151         public static final String SCREENING_BOUND = "SCREENING_BOUND";
    152         public static final String SCREENING_SENT = "SCREENING_SENT";
    153         public static final String SCREENING_COMPLETED = "SCREENING_COMPLETED";
    154         public static final String BLOCK_CHECK_INITIATED = "BLOCK_CHECK_INITIATED";
    155         public static final String BLOCK_CHECK_FINISHED = "BLOCK_CHECK_FINISHED";
    156         public static final String DIRECT_TO_VM_INITIATED = "DIRECT_TO_VM_INITIATED";
    157         public static final String DIRECT_TO_VM_FINISHED = "DIRECT_TO_VM_FINISHED";
    158         public static final String FILTERING_INITIATED = "FILTERING_INITIATED";
    159         public static final String FILTERING_COMPLETED = "FILTERING_COMPLETED";
    160         public static final String FILTERING_TIMED_OUT = "FILTERING_TIMED_OUT";
    161         public static final String REMOTELY_HELD = "REMOTELY_HELD";
    162         public static final String REMOTELY_UNHELD = "REMOTELY_UNHELD";
    163         public static final String REQUEST_PULL = "PULL";
    164         public static final String INFO = "INFO";
    165         public static final String VIDEO_STATE_CHANGED = "VIDEO_STATE_CHANGED";
    166         public static final String RECEIVE_VIDEO_REQUEST = "RECEIVE_VIDEO_REQUEST";
    167         public static final String RECEIVE_VIDEO_RESPONSE = "RECEIVE_VIDEO_RESPONSE";
    168         public static final String SEND_VIDEO_REQUEST = "SEND_VIDEO_REQUEST";
    169         public static final String SEND_VIDEO_RESPONSE = "SEND_VIDEO_RESPONSE";
    170         public static final String IS_EXTERNAL = "IS_EXTERNAL";
    171         public static final String PROPERTY_CHANGE = "PROPERTY_CHANGE";
    172         public static final String CAPABILITY_CHANGE = "CAPABILITY_CHANGE";
    173 
    174         public static class Timings {
    175             public static final String ACCEPT_TIMING = "accept";
    176             public static final String REJECT_TIMING = "reject";
    177             public static final String DISCONNECT_TIMING = "disconnect";
    178             public static final String HOLD_TIMING = "hold";
    179             public static final String UNHOLD_TIMING = "unhold";
    180             public static final String OUTGOING_TIME_TO_DIALING_TIMING = "outgoing_time_to_dialing";
    181             public static final String BIND_CS_TIMING = "bind_cs";
    182             public static final String SCREENING_COMPLETED_TIMING = "screening_completed";
    183             public static final String DIRECT_TO_VM_FINISHED_TIMING = "direct_to_vm_finished";
    184             public static final String BLOCK_CHECK_FINISHED_TIMING = "block_check_finished";
    185             public static final String FILTERING_COMPLETED_TIMING = "filtering_completed";
    186             public static final String FILTERING_TIMED_OUT_TIMING = "filtering_timed_out";
    187 
    188             private static final TimedEventPair[] sTimedEvents = {
    189                     new TimedEventPair(REQUEST_ACCEPT, SET_ACTIVE, ACCEPT_TIMING),
    190                     new TimedEventPair(REQUEST_REJECT, SET_DISCONNECTED, REJECT_TIMING),
    191                     new TimedEventPair(REQUEST_DISCONNECT, SET_DISCONNECTED, DISCONNECT_TIMING),
    192                     new TimedEventPair(REQUEST_HOLD, SET_HOLD, HOLD_TIMING),
    193                     new TimedEventPair(REQUEST_UNHOLD, SET_ACTIVE, UNHOLD_TIMING),
    194                     new TimedEventPair(START_CONNECTION, SET_DIALING,
    195                             OUTGOING_TIME_TO_DIALING_TIMING),
    196                     new TimedEventPair(BIND_CS, CS_BOUND, BIND_CS_TIMING),
    197                     new TimedEventPair(SCREENING_SENT, SCREENING_COMPLETED,
    198                             SCREENING_COMPLETED_TIMING),
    199                     new TimedEventPair(DIRECT_TO_VM_INITIATED, DIRECT_TO_VM_FINISHED,
    200                             DIRECT_TO_VM_FINISHED_TIMING),
    201                     new TimedEventPair(BLOCK_CHECK_INITIATED, BLOCK_CHECK_FINISHED,
    202                             BLOCK_CHECK_FINISHED_TIMING),
    203                     new TimedEventPair(FILTERING_INITIATED, FILTERING_COMPLETED,
    204                             FILTERING_COMPLETED_TIMING),
    205                     new TimedEventPair(FILTERING_INITIATED, FILTERING_TIMED_OUT,
    206                             FILTERING_TIMED_OUT_TIMING, 6000L),
    207             };
    208         }
    209 
    210         /**
    211          * Maps from request events to a list of possible response events. Used to track
    212          * end-to-end timing for critical user-facing operations in Telecom.
    213          */
    214         public static final Map<String, List<TimedEventPair>> requestResponsePairs;
    215         static {
    216             requestResponsePairs = new HashMap<>();
    217             for (TimedEventPair p : Timings.sTimedEvents) {
    218                 if (requestResponsePairs.containsKey(p.mRequest)) {
    219                     requestResponsePairs.get(p.mRequest).add(p);
    220                 } else {
    221                     ArrayList<TimedEventPair> responses = new ArrayList<>();
    222                     responses.add(p);
    223                     requestResponsePairs.put(p.mRequest, responses);
    224                 }
    225             }
    226         }
    227     }
    228 
    229     public static class CallEvent {
    230         public String eventId;
    231         public String sessionId;
    232         public long time;
    233         public Object data;
    234 
    235         public CallEvent(String eventId, String sessionId, long time, Object data) {
    236             this.eventId = eventId;
    237             this.sessionId = sessionId;
    238             this.time = time;
    239             this.data = data;
    240         }
    241     }
    242 
    243     public static class CallEventRecord {
    244         public static class EventTiming extends TimedEvent<String> {
    245             public String name;
    246             public long time;
    247 
    248             public EventTiming(String name, long time) {
    249                 this.name = name;
    250                 this.time = time;
    251             }
    252 
    253             public String getKey() {
    254                 return name;
    255             }
    256 
    257             public long getTime() {
    258                 return time;
    259             }
    260         }
    261 
    262         private static class PendingResponse {
    263             String requestEventId;
    264             long requestEventTimeMillis;
    265             long timeoutMillis;
    266             String name;
    267 
    268             public PendingResponse(String requestEventId, long requestEventTimeMillis,
    269                     long timeoutMillis, String name) {
    270                 this.requestEventId = requestEventId;
    271                 this.requestEventTimeMillis = requestEventTimeMillis;
    272                 this.timeoutMillis = timeoutMillis;
    273                 this.name = name;
    274             }
    275         }
    276 
    277         private static final DateFormat sLongDateFormat = new SimpleDateFormat(
    278                 "yyyy-MM-dd HH:mm:ss.SSS");
    279         private static final DateFormat sDateFormat = new SimpleDateFormat("HH:mm:ss.SSS");
    280         private final List<CallEvent> mEvents = new LinkedList<>();
    281         private final Call mCall;
    282 
    283         public CallEventRecord(Call call) {
    284             mCall = call;
    285         }
    286 
    287         public Call getCall() {
    288             return mCall;
    289         }
    290 
    291         public void addEvent(String event, String sessionId, Object data) {
    292             mEvents.add(new CallEvent(event, sessionId, System.currentTimeMillis(), data));
    293             Log.i("Event", "Call %s: %s, %s", mCall.getId(), event, data);
    294         }
    295 
    296         public List<CallEvent> getEvents() {
    297             return mEvents;
    298         }
    299 
    300         public List<EventTiming> extractEventTimings() {
    301             if (mEvents == null) {
    302                 return Collections.emptyList();
    303             }
    304 
    305             LinkedList<EventTiming> result = new LinkedList<>();
    306             Map<String, PendingResponse> pendingResponses = new HashMap<>();
    307             for (CallEvent event : mEvents) {
    308                 if (Events.requestResponsePairs.containsKey(event.eventId)) {
    309                     // This event expects a response, so add that expected response to the maps
    310                     // of pending events.
    311                     for (Events.TimedEventPair p : Events.requestResponsePairs.get(event.eventId)) {
    312                         pendingResponses.put(p.mResponse, new PendingResponse(event.eventId,
    313                                 event.time, p.mTimeoutMillis, p.mName));
    314                     }
    315                 }
    316 
    317                 PendingResponse pendingResponse = pendingResponses.remove(event.eventId);
    318                 if (pendingResponse != null) {
    319                     long elapsedTime = event.time - pendingResponse.requestEventTimeMillis;
    320                     if (elapsedTime < pendingResponse.timeoutMillis) {
    321                         result.add(new EventTiming(pendingResponse.name, elapsedTime));
    322                     }
    323                 }
    324             }
    325 
    326             return result;
    327         }
    328 
    329         public void dump(IndentingPrintWriter pw) {
    330             pw.print("Call ");
    331             pw.print(mCall.getId());
    332             pw.print(" [");
    333             pw.print(sLongDateFormat.format(new Date(mCall.getCreationTimeMillis())));
    334             pw.print("]");
    335             pw.println(mCall.isIncoming() ? "(MT - incoming)" : "(MO - outgoing)");
    336 
    337             pw.increaseIndent();
    338             pw.println("To address: " + piiHandle(mCall.getHandle()));
    339 
    340             for (CallEvent event : mEvents) {
    341                 pw.print(sDateFormat.format(new Date(event.time)));
    342                 pw.print(" - ");
    343                 pw.print(event.eventId);
    344                 if (event.data != null) {
    345                     pw.print(" (");
    346                     Object data = event.data;
    347 
    348                     if (data instanceof Call) {
    349                         // If the data is another call, then change the data to the call's CallEvent
    350                         // ID instead.
    351                         CallEventRecord record = mCallEventRecordMap.get(data);
    352                         if (record != null) {
    353                             data = "Call " + record.mCall.getId();
    354                         }
    355                     }
    356 
    357                     pw.print(data);
    358                     pw.print(")");
    359                 }
    360                 pw.print(":");
    361                 pw.print(event.sessionId);
    362                 pw.println();
    363             }
    364 
    365             pw.println("Timings (average for this call, milliseconds):");
    366             pw.increaseIndent();
    367             Map<String, Double> avgEventTimings = EventTiming.averageTimings(extractEventTimings());
    368             List<String> eventNames = new ArrayList<>(avgEventTimings.keySet());
    369             Collections.sort(eventNames);
    370             for (String eventName : eventNames) {
    371                 pw.printf("%s: %.2f\n", eventName, avgEventTimings.get(eventName));
    372             }
    373             pw.decreaseIndent();
    374             pw.decreaseIndent();
    375         }
    376     }
    377 
    378     public static final int MAX_CALLS_TO_CACHE = 10;  // Arbitrarily chosen.
    379     public static final int MAX_CALLS_TO_CACHE_DEBUG = 20;  // Arbitrarily chosen.
    380     private static final long EXTENDED_LOGGING_DURATION_MILLIS = 60000 * 30; // 30 minutes
    381 
    382     // Don't check in with this true!
    383     private static final boolean LOG_DBG = false;
    384 
    385     // Currently using 3 letters, So don't exceed 64^3
    386     private static final long SESSION_ID_ROLLOVER_THRESHOLD = 262144;
    387 
    388     // Generic tag for all In Call logging
    389     @VisibleForTesting
    390     public static String TAG = "Telecom";
    391     public static String LOGGING_TAG = "Logging";
    392 
    393     public static final boolean FORCE_LOGGING = false; /* STOP SHIP if true */
    394     public static final boolean SYSTRACE_DEBUG = false; /* STOP SHIP if true */
    395     public static final boolean DEBUG = isLoggable(android.util.Log.DEBUG);
    396     public static final boolean INFO = isLoggable(android.util.Log.INFO);
    397     public static final boolean VERBOSE = isLoggable(android.util.Log.VERBOSE);
    398     public static final boolean WARN = isLoggable(android.util.Log.WARN);
    399     public static final boolean ERROR = isLoggable(android.util.Log.ERROR);
    400 
    401     private static final Map<Call, CallEventRecord> mCallEventRecordMap = new HashMap<>();
    402     private static LinkedBlockingQueue<CallEventRecord> mCallEventRecords =
    403             new LinkedBlockingQueue<CallEventRecord>(MAX_CALLS_TO_CACHE);
    404 
    405     private static Context mContext = null;
    406     // Synchronized in all method calls
    407     private static int sCodeEntryCounter = 0;
    408     @VisibleForTesting
    409     public static ConcurrentHashMap<Integer, Session> sSessionMapper = new ConcurrentHashMap<>(100);
    410     @VisibleForTesting
    411     public static Handler sSessionCleanupHandler = new Handler(Looper.getMainLooper());
    412     @VisibleForTesting
    413     public static java.lang.Runnable sCleanStaleSessions = new java.lang.Runnable() {
    414         @Override
    415         public void run() {
    416             cleanupStaleSessions(getSessionCleanupTimeoutMs());
    417         }
    418     };
    419 
    420     // Set the logging container to be the system's. This will only change when being mocked
    421     // during testing.
    422     private static SystemLoggingContainer systemLogger = new SystemLoggingContainer();
    423 
    424     /**
    425      * Tracks whether user-activated extended logging is enabled.
    426      */
    427     private static boolean mIsUserExtendedLoggingEnabled = false;
    428 
    429     /**
    430      * The time when user-activated extended logging should be ended.  Used to determine when
    431      * extended logging should automatically be disabled.
    432      */
    433     private static long mUserExtendedLoggingStopTime = 0;
    434 
    435     private Log() {
    436     }
    437 
    438     public static void setContext(Context context) {
    439         mContext = context;
    440     }
    441 
    442     /**
    443      * Enable or disable extended telecom logging.
    444      *
    445      * @param isExtendedLoggingEnabled {@code true} if extended logging should be enabled,
    446      *          {@code false} if it should be disabled.
    447      */
    448     public static void setIsExtendedLoggingEnabled(boolean isExtendedLoggingEnabled) {
    449         // If the state hasn't changed, bail early.
    450         if (mIsUserExtendedLoggingEnabled == isExtendedLoggingEnabled) {
    451             return;
    452         }
    453 
    454         // Resize the event queue.
    455         int newSize = isExtendedLoggingEnabled ? MAX_CALLS_TO_CACHE_DEBUG : MAX_CALLS_TO_CACHE;
    456         LinkedBlockingQueue<CallEventRecord> oldEventLog = mCallEventRecords;
    457         mCallEventRecords = new LinkedBlockingQueue<CallEventRecord>(newSize);
    458         mCallEventRecordMap.clear();
    459 
    460         // Copy the existing queue into the new one.
    461         for (CallEventRecord event : oldEventLog) {
    462             addCallEventRecord(event);
    463         }
    464 
    465         mIsUserExtendedLoggingEnabled = isExtendedLoggingEnabled;
    466         if (mIsUserExtendedLoggingEnabled) {
    467             mUserExtendedLoggingStopTime = System.currentTimeMillis()
    468                     + EXTENDED_LOGGING_DURATION_MILLIS;
    469         } else {
    470             mUserExtendedLoggingStopTime = 0;
    471         }
    472     }
    473 
    474     public static final long DEFAULT_SESSION_TIMEOUT_MS = 30000L; // 30 seconds
    475     private static MessageDigest sMessageDigest;
    476 
    477     public static void initMd5Sum() {
    478         new AsyncTask<Void, Void, Void>() {
    479             @Override
    480             public Void doInBackground(Void... args) {
    481                 MessageDigest md;
    482                 try {
    483                     md = MessageDigest.getInstance("SHA-1");
    484                 } catch (NoSuchAlgorithmException e) {
    485                     md = null;
    486                 }
    487                 sMessageDigest = md;
    488                 return null;
    489             }
    490         }.execute();
    491     }
    492 
    493     @VisibleForTesting
    494     public static void setTag(String tag) {
    495         TAG = tag;
    496     }
    497 
    498     @VisibleForTesting
    499     public static void setLoggingContainer(SystemLoggingContainer logger) {
    500         systemLogger = logger;
    501     }
    502 
    503     // Overridden in LogTest to skip query to ContentProvider
    504     public interface ISessionCleanupTimeoutMs {
    505         long get();
    506     }
    507     @VisibleForTesting
    508     public static ISessionCleanupTimeoutMs sSessionCleanupTimeoutMs =
    509             new ISessionCleanupTimeoutMs() {
    510                 @Override
    511                 public long get() {
    512                     // mContext will be null if Log is called from another process
    513                     // (UserCallActivity, for example). For these cases, use the default value.
    514                     if(mContext == null) {
    515                         return DEFAULT_SESSION_TIMEOUT_MS;
    516                     }
    517                     return Timeouts.getStaleSessionCleanupTimeoutMillis(
    518                             mContext.getContentResolver());
    519                 }
    520             };
    521 
    522     private static long getSessionCleanupTimeoutMs() {
    523         return sSessionCleanupTimeoutMs.get();
    524     }
    525 
    526     private static synchronized void resetStaleSessionTimer() {
    527         sSessionCleanupHandler.removeCallbacksAndMessages(null);
    528         // Will be null in Log Testing
    529         if (sCleanStaleSessions != null) {
    530             sSessionCleanupHandler.postDelayed(sCleanStaleSessions, getSessionCleanupTimeoutMs());
    531         }
    532     }
    533 
    534     /**
    535      * Call at an entry point to the Telecom code to track the session. This code must be
    536      * accompanied by a Log.endSession().
    537      */
    538     public static synchronized void startSession(String shortMethodName) {
    539         startSession(shortMethodName, null);
    540     }
    541     public static synchronized void startSession(String shortMethodName,
    542             String callerIdentification) {
    543         resetStaleSessionTimer();
    544         int threadId = getCallingThreadId();
    545         Session activeSession = sSessionMapper.get(threadId);
    546         // We have called startSession within an active session that has not ended... Register this
    547         // session as a subsession.
    548         if (activeSession != null) {
    549             Session childSession = createSubsession(true);
    550             continueSession(childSession, shortMethodName);
    551             return;
    552         }
    553         Session newSession = new Session(getNextSessionID(), shortMethodName,
    554                 System.currentTimeMillis(), threadId, false, callerIdentification);
    555         sSessionMapper.put(threadId, newSession);
    556 
    557         Log.v(LOGGING_TAG, Session.START_SESSION);
    558     }
    559 
    560 
    561     /**
    562      * Notifies the logging system that a subsession will be run at a later point and
    563      * allocates the resources. Returns a session object that must be used in
    564      * Log.continueSession(...) to start the subsession.
    565      */
    566     public static Session createSubsession() {
    567         return createSubsession(false);
    568     }
    569 
    570     private static synchronized Session createSubsession(boolean isStartedFromActiveSession) {
    571         int threadId = getCallingThreadId();
    572         Session threadSession = sSessionMapper.get(threadId);
    573         if (threadSession == null) {
    574             Log.d(LOGGING_TAG, "Log.createSubsession was called with no session active.");
    575             return null;
    576         }
    577         // Start execution time of the session will be overwritten in continueSession(...).
    578         Session newSubsession = new Session(threadSession.getNextChildId(),
    579                 threadSession.getShortMethodName(), System.currentTimeMillis(), threadId,
    580                 isStartedFromActiveSession, null);
    581         threadSession.addChild(newSubsession);
    582         newSubsession.setParentSession(threadSession);
    583 
    584         if(!isStartedFromActiveSession) {
    585             Log.v(LOGGING_TAG, Session.CREATE_SUBSESSION + " " + newSubsession.toString());
    586         } else {
    587             Log.v(LOGGING_TAG, Session.CREATE_SUBSESSION + " (Invisible subsession)");
    588         }
    589         return newSubsession;
    590     }
    591 
    592     /**
    593      * Cancels a subsession that had Log.createSubsession() called on it, but will never have
    594      * Log.continueSession(...) called on it due to an error. Allows the subsession to be cleaned
    595      * gracefully instead of being removed by the sSessionCleanupHandler forcefully later.
    596      */
    597     public static synchronized void cancelSubsession(Session subsession) {
    598         if (subsession == null) {
    599             return;
    600         }
    601 
    602         subsession.markSessionCompleted(0);
    603         endParentSessions(subsession);
    604     }
    605 
    606     /**
    607      * Starts the subsession that was created in Log.CreateSubsession. The Log.endSession() method
    608      * must be called at the end of this method. The full session will complete when all
    609      * subsessions are completed.
    610      */
    611     public static synchronized void continueSession(Session subsession, String shortMethodName) {
    612         if (subsession == null) {
    613             return;
    614         }
    615         resetStaleSessionTimer();
    616         String callingMethodName = subsession.getShortMethodName();
    617         subsession.setShortMethodName(callingMethodName + "->" + shortMethodName);
    618         subsession.setExecutionStartTimeMs(System.currentTimeMillis());
    619         Session parentSession = subsession.getParentSession();
    620         if (parentSession == null) {
    621             Log.d(LOGGING_TAG, "Log.continueSession was called with no session active for " +
    622                     "method %s.", shortMethodName);
    623             return;
    624         }
    625 
    626         sSessionMapper.put(getCallingThreadId(), subsession);
    627         if(!subsession.isStartedFromActiveSession()) {
    628             Log.v(LOGGING_TAG, Session.CONTINUE_SUBSESSION);
    629         } else {
    630             Log.v(LOGGING_TAG, Session.CONTINUE_SUBSESSION + " (Invisible Subsession) with " +
    631                     "Method " + shortMethodName);
    632         }
    633     }
    634 
    635     public static void checkIsThreadLogged() {
    636         int threadId = getCallingThreadId();
    637         Session threadSession = sSessionMapper.get(threadId);
    638         if (threadSession == null) {
    639             android.util.Log.e(LOGGING_TAG, "Logging Thread Check Failed!", new Exception());
    640         }
    641     }
    642 
    643     /**
    644      * Ends the current session/subsession. Must be called after a Log.startSession(...) and
    645      * Log.continueSession(...) call.
    646      */
    647     public static synchronized void endSession() {
    648         int threadId = getCallingThreadId();
    649         Session completedSession = sSessionMapper.get(threadId);
    650         if (completedSession == null) {
    651             Log.w(LOGGING_TAG, "Log.endSession was called with no session active.");
    652             return;
    653         }
    654 
    655         completedSession.markSessionCompleted(System.currentTimeMillis());
    656         if(!completedSession.isStartedFromActiveSession()) {
    657             Log.v(LOGGING_TAG, Session.END_SUBSESSION + " (dur: " +
    658                     completedSession.getLocalExecutionTime() + " mS)");
    659         } else {
    660             Log.v(LOGGING_TAG, Session.END_SUBSESSION + " (Invisible Subsession) (dur: " +
    661                     completedSession.getLocalExecutionTime() + " mS)");
    662         }
    663         // Remove after completed so that reference still exists for logging the end events
    664         Session parentSession = completedSession.getParentSession();
    665         sSessionMapper.remove(threadId);
    666         endParentSessions(completedSession);
    667         // If this subsession was started from a parent session using Log.startSession, return the
    668         // ThreadID back to the parent after completion.
    669         if (parentSession != null && !parentSession.isSessionCompleted() &&
    670                 completedSession.isStartedFromActiveSession()) {
    671             sSessionMapper.put(threadId, parentSession);
    672         }
    673     }
    674 
    675     // Recursively deletes all complete parent sessions of the current subsession if it is a leaf.
    676     private static void endParentSessions(Session subsession) {
    677         // Session is not completed or not currently a leaf, so we can not remove because a child is
    678         // still running
    679         if (!subsession.isSessionCompleted() || subsession.getChildSessions().size() != 0) {
    680             return;
    681         }
    682 
    683         Session parentSession = subsession.getParentSession();
    684         if (parentSession != null) {
    685             subsession.setParentSession(null);
    686             parentSession.removeChild(subsession);
    687             endParentSessions(parentSession);
    688         } else {
    689             // All of the subsessions have been completed and it is time to report on the full
    690             // running time of the session.
    691             long fullSessionTimeMs =
    692                     System.currentTimeMillis() - subsession.getExecutionStartTimeMilliseconds();
    693             Analytics.addSessionTiming(subsession.getShortMethodName(), fullSessionTimeMs);
    694             Log.v(LOGGING_TAG, Session.END_SESSION + " (dur: " + fullSessionTimeMs + " ms): " +
    695                     subsession.toString());
    696         }
    697     }
    698 
    699     private synchronized static String getNextSessionID() {
    700         Integer nextId = sCodeEntryCounter++;
    701         if (nextId >= SESSION_ID_ROLLOVER_THRESHOLD) {
    702             restartSessionCounter();
    703             nextId = sCodeEntryCounter++;
    704         }
    705         return getBase64Encoding(nextId);
    706     }
    707 
    708     @VisibleForTesting
    709     public synchronized static void restartSessionCounter() {
    710         sCodeEntryCounter = 0;
    711     }
    712 
    713     @VisibleForTesting
    714     public static String getBase64Encoding(int number) {
    715         byte[] idByteArray = ByteBuffer.allocate(4).putInt(number).array();
    716         idByteArray = Arrays.copyOfRange(idByteArray, 2, 4);
    717         return Base64.encodeToString(idByteArray, Base64.NO_WRAP | Base64.NO_PADDING);
    718     }
    719 
    720     public static int getCallingThreadId() {
    721         return android.os.Process.myTid();
    722     }
    723 
    724     public static void event(Call call, String event) {
    725         event(call, event, null);
    726     }
    727 
    728     public static void event(Call call, String event, Object data) {
    729         Session currentSession = sSessionMapper.get(getCallingThreadId());
    730         String currentSessionID = currentSession != null ? currentSession.toString() : "";
    731 
    732         if (call == null) {
    733             Log.i(TAG, "Non-call EVENT: %s, %s", event, data);
    734             return;
    735         }
    736         synchronized (mCallEventRecords) {
    737             if (!mCallEventRecordMap.containsKey(call)) {
    738                 CallEventRecord newRecord = new CallEventRecord(call);
    739                 addCallEventRecord(newRecord);
    740             }
    741 
    742             CallEventRecord record = mCallEventRecordMap.get(call);
    743             record.addEvent(event, currentSessionID, data);
    744         }
    745     }
    746 
    747     public static void event(Call call, String event, String format, Object... args) {
    748         String msg;
    749         try {
    750             msg = (args == null || args.length == 0) ? format
    751                     : String.format(Locale.US, format, args);
    752         } catch (IllegalFormatException ife) {
    753             e("Log", ife, "IllegalFormatException: formatString='%s' numArgs=%d", format,
    754                     args.length);
    755             msg = format + " (An error occurred while formatting the message.)";
    756         }
    757 
    758         event(call, event, msg);
    759     }
    760 
    761     @VisibleForTesting
    762     public static synchronized void cleanupStaleSessions(long timeoutMs) {
    763         String logMessage = "Stale Sessions Cleaned:\n";
    764         boolean isSessionsStale = false;
    765         long currentTimeMs = System.currentTimeMillis();
    766         // Remove references that are in the Session Mapper (causing GC to occur) on
    767         // sessions that are lasting longer than LOGGING_SESSION_TIMEOUT_MS.
    768         // If this occurs, then there is most likely a Session active that never had
    769         // Log.endSession called on it.
    770         for (Iterator<ConcurrentHashMap.Entry<Integer, Session>> it =
    771              sSessionMapper.entrySet().iterator(); it.hasNext(); ) {
    772             ConcurrentHashMap.Entry<Integer, Session> entry = it.next();
    773             Session session = entry.getValue();
    774             if (currentTimeMs - session.getExecutionStartTimeMilliseconds() > timeoutMs) {
    775                 it.remove();
    776                 logMessage += session.printFullSessionTree() + "\n";
    777                 isSessionsStale = true;
    778             }
    779         }
    780         if (isSessionsStale) {
    781             Log.w(LOGGING_TAG, logMessage);
    782         } else {
    783             Log.v(LOGGING_TAG, "No stale logging sessions needed to be cleaned...");
    784         }
    785     }
    786 
    787     private static void addCallEventRecord(CallEventRecord newRecord) {
    788         Call call = newRecord.getCall();
    789 
    790         // First remove the oldest entry if no new ones exist.
    791         if (mCallEventRecords.remainingCapacity() == 0) {
    792             CallEventRecord record = mCallEventRecords.poll();
    793             if (record != null) {
    794                 mCallEventRecordMap.remove(record.getCall());
    795             }
    796         }
    797 
    798         // Now add a new entry
    799         mCallEventRecords.add(newRecord);
    800         mCallEventRecordMap.put(call, newRecord);
    801 
    802         // Register the events with Analytics
    803         if (call.getAnalytics() != null) {
    804             call.getAnalytics().setCallEvents(newRecord);
    805         } else {
    806             Log.w(LOGGING_TAG, "Call analytics is null");
    807         }
    808     }
    809 
    810     /**
    811      * If user enabled extended logging is enabled and the time limit has passed, disables the
    812      * extended logging.
    813      */
    814     private static void maybeDisableLogging() {
    815         if (!mIsUserExtendedLoggingEnabled) {
    816             return;
    817         }
    818 
    819         if (mUserExtendedLoggingStopTime < System.currentTimeMillis()) {
    820             mUserExtendedLoggingStopTime = 0;
    821             mIsUserExtendedLoggingEnabled = false;
    822         }
    823     }
    824 
    825     public static boolean isLoggable(int level) {
    826         return FORCE_LOGGING || android.util.Log.isLoggable(TAG, level);
    827     }
    828 
    829     public static void d(String prefix, String format, Object... args) {
    830         if (mIsUserExtendedLoggingEnabled) {
    831             maybeDisableLogging();
    832             systemLogger.i(TAG, buildMessage(prefix, format, args));
    833         } else if (DEBUG) {
    834             systemLogger.d(TAG, buildMessage(prefix, format, args));
    835         }
    836     }
    837 
    838     public static void d(Object objectPrefix, String format, Object... args) {
    839         if (mIsUserExtendedLoggingEnabled) {
    840             maybeDisableLogging();
    841             systemLogger.i(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args));
    842         } else if (DEBUG) {
    843             systemLogger.d(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args));
    844         }
    845     }
    846 
    847     public static void i(String prefix, String format, Object... args) {
    848         if (INFO) {
    849             systemLogger.i(TAG, buildMessage(prefix, format, args));
    850         }
    851     }
    852 
    853     public static void i(Object objectPrefix, String format, Object... args) {
    854         if (INFO) {
    855             systemLogger.i(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args));
    856         }
    857     }
    858 
    859     public static void v(String prefix, String format, Object... args) {
    860         if (mIsUserExtendedLoggingEnabled) {
    861             maybeDisableLogging();
    862             systemLogger.i(TAG, buildMessage(prefix, format, args));
    863         } else if (VERBOSE) {
    864             systemLogger.v(TAG, buildMessage(prefix, format, args));
    865         }
    866     }
    867 
    868     public static void v(Object objectPrefix, String format, Object... args) {
    869         if (mIsUserExtendedLoggingEnabled) {
    870             maybeDisableLogging();
    871             systemLogger.i(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args));
    872         } else if (VERBOSE) {
    873             systemLogger.v(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args));
    874         }
    875     }
    876 
    877     public static void w(String prefix, String format, Object... args) {
    878         if (WARN) {
    879             systemLogger.w(TAG, buildMessage(prefix, format, args));
    880         }
    881     }
    882 
    883     public static void w(Object objectPrefix, String format, Object... args) {
    884         if (WARN) {
    885             systemLogger.w(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args));
    886         }
    887     }
    888 
    889     public static void e(String prefix, Throwable tr, String format, Object... args) {
    890         if (ERROR) {
    891             systemLogger.e(TAG, buildMessage(prefix, format, args), tr);
    892         }
    893     }
    894 
    895     public static void e(Object objectPrefix, Throwable tr, String format, Object... args) {
    896         if (ERROR) {
    897             systemLogger.e(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args),
    898                     tr);
    899         }
    900     }
    901 
    902     public static void wtf(String prefix, Throwable tr, String format, Object... args) {
    903         systemLogger.wtf(TAG, buildMessage(prefix, format, args), tr);
    904     }
    905 
    906     public static void wtf(Object objectPrefix, Throwable tr, String format, Object... args) {
    907         systemLogger.wtf(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args),
    908                 tr);
    909     }
    910 
    911     public static void wtf(String prefix, String format, Object... args) {
    912         String msg = buildMessage(prefix, format, args);
    913         systemLogger.wtf(TAG, msg, new IllegalStateException(msg));
    914     }
    915 
    916     public static void wtf(Object objectPrefix, String format, Object... args) {
    917         String msg = buildMessage(getPrefixFromObject(objectPrefix), format, args);
    918         systemLogger.wtf(TAG, msg, new IllegalStateException(msg));
    919     }
    920 
    921     public static String piiHandle(Object pii) {
    922         if (pii == null || VERBOSE) {
    923             return String.valueOf(pii);
    924         }
    925 
    926         StringBuilder sb = new StringBuilder();
    927         if (pii instanceof Uri) {
    928             Uri uri = (Uri) pii;
    929             String scheme = uri.getScheme();
    930 
    931             if (!TextUtils.isEmpty(scheme)) {
    932                 sb.append(scheme).append(":");
    933             }
    934 
    935             String textToObfuscate = uri.getSchemeSpecificPart();
    936             if (PhoneAccount.SCHEME_TEL.equals(scheme)) {
    937                 for (int i = 0; i < textToObfuscate.length(); i++) {
    938                     char c = textToObfuscate.charAt(i);
    939                     sb.append(PhoneNumberUtils.isDialable(c) ? "*" : c);
    940                 }
    941             } else if (PhoneAccount.SCHEME_SIP.equals(scheme)) {
    942                 for (int i = 0; i < textToObfuscate.length(); i++) {
    943                     char c = textToObfuscate.charAt(i);
    944                     if (c != '@' && c != '.') {
    945                         c = '*';
    946                     }
    947                     sb.append(c);
    948                 }
    949             } else {
    950                 sb.append(pii(pii));
    951             }
    952         }
    953 
    954         return sb.toString();
    955     }
    956 
    957     /**
    958      * Redact personally identifiable information for production users.
    959      * If we are running in verbose mode, return the original string, otherwise
    960      * return a SHA-1 hash of the input string.
    961      */
    962     public static String pii(Object pii) {
    963         if (pii == null || VERBOSE) {
    964             return String.valueOf(pii);
    965         }
    966         return "[" + secureHash(String.valueOf(pii).getBytes()) + "]";
    967     }
    968 
    969     public static void dumpCallEvents(IndentingPrintWriter pw) {
    970         pw.println("Historical Calls:");
    971         pw.increaseIndent();
    972         for (CallEventRecord callEventRecord : mCallEventRecords) {
    973             callEventRecord.dump(pw);
    974         }
    975         pw.decreaseIndent();
    976     }
    977 
    978     private static String secureHash(byte[] input) {
    979         if (sMessageDigest != null) {
    980             sMessageDigest.reset();
    981             sMessageDigest.update(input);
    982             byte[] result = sMessageDigest.digest();
    983             return encodeHex(result);
    984         } else {
    985             return "Uninitialized SHA1";
    986         }
    987     }
    988 
    989     private static String encodeHex(byte[] bytes) {
    990         StringBuffer hex = new StringBuffer(bytes.length * 2);
    991 
    992         for (int i = 0; i < bytes.length; i++) {
    993             int byteIntValue = bytes[i] & 0xff;
    994             if (byteIntValue < 0x10) {
    995                 hex.append("0");
    996             }
    997             hex.append(Integer.toString(byteIntValue, 16));
    998         }
    999 
   1000         return hex.toString();
   1001     }
   1002 
   1003     private static String getPrefixFromObject(Object obj) {
   1004         return obj == null ? "<null>" : obj.getClass().getSimpleName();
   1005     }
   1006 
   1007     private static String buildMessage(String prefix, String format, Object... args) {
   1008         if (LOG_DBG) {
   1009             checkIsThreadLogged();
   1010         }
   1011         // Incorporate thread ID and calling method into prefix
   1012         String sessionPostfix = "";
   1013         Session currentSession = sSessionMapper.get(getCallingThreadId());
   1014         if (currentSession != null) {
   1015             sessionPostfix = ": " + currentSession.toString();
   1016         }
   1017 
   1018         String msg;
   1019         try {
   1020             msg = (args == null || args.length == 0) ? format
   1021                     : String.format(Locale.US, format, args);
   1022         } catch (IllegalFormatException ife) {
   1023             e("Log", ife, "IllegalFormatException: formatString='%s' numArgs=%d", format,
   1024                     args.length);
   1025             msg = format + " (An error occurred while formatting the message.)";
   1026         }
   1027         return String.format(Locale.US, "%s: %s%s", prefix, msg, sessionPostfix);
   1028     }
   1029 }
   1030