| /* |
| * Copyright 2014, The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.telecom; |
| |
| import android.annotation.NonNull; |
| import android.compat.annotation.UnsupportedAppUsage; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.telecom.Logging.EventManager; |
| import android.telecom.Logging.Session; |
| import android.telecom.Logging.SessionManager; |
| import android.telephony.PhoneNumberUtils; |
| import android.text.TextUtils; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.util.IndentingPrintWriter; |
| |
| import java.util.Arrays; |
| import java.util.IllegalFormatException; |
| import java.util.Locale; |
| import java.util.stream.Collectors; |
| |
| /** |
| * Manages logging for the entire module. |
| * |
| * @hide |
| */ |
| public class Log { |
| |
| private static final long EXTENDED_LOGGING_DURATION_MILLIS = 60000 * 30; // 30 minutes |
| |
| private static final int EVENTS_TO_CACHE = 10; |
| private static final int EVENTS_TO_CACHE_DEBUG = 20; |
| |
| /** |
| * When generating a bug report, include the last X dialable digits when logging phone numbers. |
| */ |
| private static final int NUM_DIALABLE_DIGITS_TO_LOG = Build.IS_USER ? 0 : 2; |
| |
| // Generic tag for all Telecom logging |
| @VisibleForTesting |
| public static String TAG = "TelecomFramework"; |
| public static boolean DEBUG = isLoggable(android.util.Log.DEBUG); |
| public static boolean INFO = isLoggable(android.util.Log.INFO); |
| public static boolean VERBOSE = isLoggable(android.util.Log.VERBOSE); |
| public static boolean WARN = isLoggable(android.util.Log.WARN); |
| public static boolean ERROR = isLoggable(android.util.Log.ERROR); |
| |
| private static final boolean FORCE_LOGGING = false; /* STOP SHIP if true */ |
| private static final boolean USER_BUILD = Build.IS_USER; |
| |
| // Used to synchronize singleton logging lazy initialization |
| private static final Object sSingletonSync = new Object(); |
| private static EventManager sEventManager; |
| private static SessionManager sSessionManager; |
| private static Object sLock = null; |
| |
| /** |
| * Tracks whether user-activated extended logging is enabled. |
| */ |
| private static boolean sIsUserExtendedLoggingEnabled = false; |
| |
| /** |
| * Enabled in telecom testing to help gate log statements causing log spew. |
| */ |
| private static boolean sIsUnitTestingEnabled = false; |
| |
| /** |
| * The time when user-activated extended logging should be ended. Used to determine when |
| * extended logging should automatically be disabled. |
| */ |
| private static long sUserExtendedLoggingStopTime = 0; |
| |
| private Log() { |
| } |
| |
| public static void d(String prefix, String format, Object... args) { |
| if (sIsUserExtendedLoggingEnabled) { |
| maybeDisableLogging(); |
| android.util.Slog.i(TAG, buildMessage(prefix, format, args)); |
| } else if (DEBUG) { |
| android.util.Slog.d(TAG, buildMessage(prefix, format, args)); |
| } |
| } |
| |
| public static void d(Object objectPrefix, String format, Object... args) { |
| if (sIsUserExtendedLoggingEnabled) { |
| maybeDisableLogging(); |
| android.util.Slog.i(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args)); |
| } else if (DEBUG) { |
| android.util.Slog.d(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args)); |
| } |
| } |
| |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) |
| public static void i(String prefix, String format, Object... args) { |
| if (INFO) { |
| android.util.Slog.i(TAG, buildMessage(prefix, format, args)); |
| } |
| } |
| |
| public static void i(Object objectPrefix, String format, Object... args) { |
| if (INFO) { |
| android.util.Slog.i(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args)); |
| } |
| } |
| |
| public static void v(String prefix, String format, Object... args) { |
| if (sIsUserExtendedLoggingEnabled) { |
| maybeDisableLogging(); |
| android.util.Slog.i(TAG, buildMessage(prefix, format, args)); |
| } else if (VERBOSE) { |
| android.util.Slog.v(TAG, buildMessage(prefix, format, args)); |
| } |
| } |
| |
| public static void v(Object objectPrefix, String format, Object... args) { |
| if (sIsUserExtendedLoggingEnabled) { |
| maybeDisableLogging(); |
| android.util.Slog.i(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args)); |
| } else if (VERBOSE) { |
| android.util.Slog.v(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args)); |
| } |
| } |
| |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) |
| public static void w(String prefix, String format, Object... args) { |
| if (WARN) { |
| android.util.Slog.w(TAG, buildMessage(prefix, format, args)); |
| } |
| } |
| |
| public static void w(Object objectPrefix, String format, Object... args) { |
| if (WARN) { |
| android.util.Slog.w(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args)); |
| } |
| } |
| |
| public static void e(String prefix, Throwable tr, String format, Object... args) { |
| if (ERROR) { |
| android.util.Slog.e(TAG, buildMessage(prefix, format, args), tr); |
| } |
| } |
| |
| public static void e(Object objectPrefix, Throwable tr, String format, Object... args) { |
| if (ERROR) { |
| android.util.Slog.e(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args), |
| tr); |
| } |
| } |
| |
| public static void wtf(String prefix, Throwable tr, String format, Object... args) { |
| android.util.Slog.wtf(TAG, buildMessage(prefix, format, args), tr); |
| } |
| |
| public static void wtf(Object objectPrefix, Throwable tr, String format, Object... args) { |
| android.util.Slog.wtf(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args), |
| tr); |
| } |
| |
| public static void wtf(String prefix, String format, Object... args) { |
| String msg = buildMessage(prefix, format, args); |
| android.util.Slog.wtf(TAG, msg, new IllegalStateException(msg)); |
| } |
| |
| public static void wtf(Object objectPrefix, String format, Object... args) { |
| String msg = buildMessage(getPrefixFromObject(objectPrefix), format, args); |
| android.util.Slog.wtf(TAG, msg, new IllegalStateException(msg)); |
| } |
| |
| /** |
| * The ease of use methods below only act mostly as proxies to the Session and Event Loggers. |
| * They also control the lazy loaders of the singleton instances, which will never be loaded if |
| * the proxy methods aren't used. |
| * |
| * Please see each method's documentation inside of their respective implementations in the |
| * loggers. |
| */ |
| |
| public static void setSessionContext(Context context) { |
| getSessionManager().setContext(context); |
| } |
| |
| public static void startSession(String shortMethodName) { |
| getSessionManager().startSession(shortMethodName, null); |
| } |
| |
| public static void startSession(Session.Info info, String shortMethodName) { |
| getSessionManager().startSession(info, shortMethodName, null); |
| } |
| |
| public static void startSession(String shortMethodName, String callerIdentification) { |
| getSessionManager().startSession(shortMethodName, callerIdentification); |
| } |
| |
| public static void startSession(Session.Info info, String shortMethodName, |
| String callerIdentification) { |
| getSessionManager().startSession(info, shortMethodName, callerIdentification); |
| } |
| |
| public static Session createSubsession() { |
| return getSessionManager().createSubsession(); |
| } |
| |
| public static Session.Info getExternalSession() { |
| return getSessionManager().getExternalSession(); |
| } |
| |
| /** |
| * Retrieves external session information, providing a context for the recipient of the session |
| * info where the external session came from. |
| * @param ownerInfo The external owner info. |
| * @return New {@link Session.Info} instance with owner info set. |
| */ |
| public static Session.Info getExternalSession(@NonNull String ownerInfo) { |
| return getSessionManager().getExternalSession(ownerInfo); |
| } |
| |
| public static void cancelSubsession(Session subsession) { |
| getSessionManager().cancelSubsession(subsession); |
| } |
| |
| public static void continueSession(Session subsession, String shortMethodName) { |
| getSessionManager().continueSession(subsession, shortMethodName); |
| } |
| |
| public static void endSession() { |
| getSessionManager().endSession(); |
| } |
| |
| public static void registerSessionListener(SessionManager.ISessionListener l) { |
| getSessionManager().registerSessionListener(l); |
| } |
| |
| public static String getSessionId() { |
| // If the Session logger has not been initialized, then there have been no sessions logged. |
| // Don't load it now! |
| synchronized (sSingletonSync) { |
| if (sSessionManager != null) { |
| return getSessionManager().getSessionId(); |
| } else { |
| return ""; |
| } |
| } |
| } |
| |
| public static void addEvent(EventManager.Loggable recordEntry, String event) { |
| getEventManager().event(recordEntry, event, null); |
| } |
| |
| public static void addEvent(EventManager.Loggable recordEntry, String event, Object data) { |
| getEventManager().event(recordEntry, event, data); |
| } |
| |
| public static void addEvent(EventManager.Loggable recordEntry, String event, String format, |
| Object... args) { |
| getEventManager().event(recordEntry, event, format, args); |
| } |
| |
| public static void registerEventListener(EventManager.EventListener e) { |
| getEventManager().registerEventListener(e); |
| } |
| |
| public static void addRequestResponsePair(EventManager.TimedEventPair p) { |
| getEventManager().addRequestResponsePair(p); |
| } |
| |
| public static void dumpEvents(IndentingPrintWriter pw) { |
| // If the Events logger has not been initialized, then there have been no events logged. |
| // Don't load it now! |
| synchronized (sSingletonSync) { |
| if (sEventManager != null) { |
| getEventManager().dumpEvents(pw); |
| } else { |
| pw.println("No Historical Events Logged."); |
| } |
| } |
| } |
| |
| /** |
| * Dumps the events in a timeline format. |
| * @param pw The {@link IndentingPrintWriter} to write to. |
| * @hide |
| */ |
| public static void dumpEventsTimeline(IndentingPrintWriter pw) { |
| // If the Events logger has not been initialized, then there have been no events logged. |
| // Don't load it now! |
| synchronized (sSingletonSync) { |
| if (sEventManager != null) { |
| getEventManager().dumpEventsTimeline(pw); |
| } else { |
| pw.println("No Historical Events Logged."); |
| } |
| } |
| } |
| |
| /** |
| * Enable or disable extended telecom logging. |
| * |
| * @param isExtendedLoggingEnabled {@code true} if extended logging should be enabled, |
| * {@code false} if it should be disabled. |
| */ |
| public static void setIsExtendedLoggingEnabled(boolean isExtendedLoggingEnabled) { |
| // If the state hasn't changed, bail early. |
| if (sIsUserExtendedLoggingEnabled == isExtendedLoggingEnabled) { |
| return; |
| } |
| |
| if (sEventManager != null) { |
| sEventManager.changeEventCacheSize(isExtendedLoggingEnabled ? |
| EVENTS_TO_CACHE_DEBUG : EVENTS_TO_CACHE); |
| } |
| |
| sIsUserExtendedLoggingEnabled = isExtendedLoggingEnabled; |
| if (sIsUserExtendedLoggingEnabled) { |
| sUserExtendedLoggingStopTime = System.currentTimeMillis() |
| + EXTENDED_LOGGING_DURATION_MILLIS; |
| } else { |
| sUserExtendedLoggingStopTime = 0; |
| } |
| } |
| |
| /** |
| * Enabled when tests are running to help gate log statements causing log spew. |
| * |
| * @param isEnabled {@code true} if running unit tests. false otherwise. |
| * |
| */ |
| public static void setUnitTestingEnabled(boolean isEnabled) { |
| sIsUnitTestingEnabled = isEnabled; |
| } |
| |
| public static boolean isUnitTestingEnabled() { |
| return sIsUnitTestingEnabled; |
| } |
| |
| private static EventManager getEventManager() { |
| // Checking for null again outside of synchronization because we only need to synchronize |
| // during the lazy loading of the events logger. We don't need to synchronize elsewhere. |
| if (sEventManager == null) { |
| synchronized (sSingletonSync) { |
| if (sEventManager == null) { |
| sEventManager = new EventManager(Log::getSessionId); |
| return sEventManager; |
| } |
| } |
| } |
| return sEventManager; |
| } |
| |
| @VisibleForTesting |
| public static SessionManager getSessionManager() { |
| // Checking for null again outside of synchronization because we only need to synchronize |
| // during the lazy loading of the session logger. We don't need to synchronize elsewhere. |
| if (sSessionManager == null) { |
| synchronized (sSingletonSync) { |
| if (sSessionManager == null) { |
| sSessionManager = new SessionManager(); |
| return sSessionManager; |
| } |
| } |
| } |
| return sSessionManager; |
| } |
| |
| public static void setTag(String tag) { |
| TAG = tag; |
| DEBUG = isLoggable(android.util.Log.DEBUG); |
| INFO = isLoggable(android.util.Log.INFO); |
| VERBOSE = isLoggable(android.util.Log.VERBOSE); |
| WARN = isLoggable(android.util.Log.WARN); |
| ERROR = isLoggable(android.util.Log.ERROR); |
| } |
| |
| /** |
| * Sets the main telecom sync lock used within Telecom. This is used when building log messages |
| * so that we can identify places in the code where we are doing something outside of the |
| * Telecom lock. |
| * @param lock The lock. |
| */ |
| public static void setLock(Object lock) { |
| // Don't do lock monitoring on user builds. |
| if (!Build.IS_USER) { |
| sLock = lock; |
| } |
| } |
| |
| /** |
| * If user enabled extended logging is enabled and the time limit has passed, disables the |
| * extended logging. |
| */ |
| private static void maybeDisableLogging() { |
| if (!sIsUserExtendedLoggingEnabled) { |
| return; |
| } |
| |
| if (sUserExtendedLoggingStopTime < System.currentTimeMillis()) { |
| sUserExtendedLoggingStopTime = 0; |
| sIsUserExtendedLoggingEnabled = false; |
| } |
| } |
| |
| public static boolean isLoggable(int level) { |
| return FORCE_LOGGING || android.util.Log.isLoggable(TAG, level); |
| } |
| |
| /** |
| * Generates an obfuscated string for a calling handle in {@link Uri} format, or a raw phone |
| * phone number in {@link String} format. |
| * @param pii The information to obfuscate. |
| * @return The obfuscated string. |
| */ |
| public static String piiHandle(Object pii) { |
| if (pii == null || VERBOSE) { |
| return String.valueOf(pii); |
| } |
| |
| StringBuilder sb = new StringBuilder(); |
| if (pii instanceof Uri) { |
| Uri uri = (Uri) pii; |
| String scheme = uri.getScheme(); |
| |
| if (!TextUtils.isEmpty(scheme)) { |
| sb.append(scheme).append(":"); |
| } |
| |
| String textToObfuscate = uri.getSchemeSpecificPart(); |
| if (PhoneAccount.SCHEME_TEL.equals(scheme)) { |
| obfuscatePhoneNumber(sb, textToObfuscate); |
| } else if (PhoneAccount.SCHEME_SIP.equals(scheme)) { |
| for (int i = 0; i < textToObfuscate.length(); i++) { |
| char c = textToObfuscate.charAt(i); |
| if (c != '@' && c != '.') { |
| c = '*'; |
| } |
| sb.append(c); |
| } |
| } else { |
| sb.append(pii(pii)); |
| } |
| } else if (pii instanceof String) { |
| String number = (String) pii; |
| obfuscatePhoneNumber(sb, number); |
| } |
| |
| return sb.toString(); |
| } |
| |
| /** |
| * Obfuscates a phone number, allowing NUM_DIALABLE_DIGITS_TO_LOG digits to be exposed for the |
| * phone number. |
| * @param sb String buffer to write obfuscated number to. |
| * @param phoneNumber The number to obfuscate. |
| */ |
| private static void obfuscatePhoneNumber(StringBuilder sb, String phoneNumber) { |
| int numDigitsToObfuscate = getDialableCount(phoneNumber) |
| - NUM_DIALABLE_DIGITS_TO_LOG; |
| for (int i = 0; i < phoneNumber.length(); i++) { |
| char c = phoneNumber.charAt(i); |
| boolean isDialable = PhoneNumberUtils.isDialable(c); |
| if (isDialable) { |
| numDigitsToObfuscate--; |
| } |
| sb.append(isDialable && numDigitsToObfuscate >= 0 ? "*" : c); |
| } |
| } |
| |
| /** |
| * Determines the number of dialable characters in a string. |
| * @param toCount The string to count dialable characters in. |
| * @return The count of dialable characters. |
| */ |
| private static int getDialableCount(String toCount) { |
| int numDialable = 0; |
| for (char c : toCount.toCharArray()) { |
| if (PhoneNumberUtils.isDialable(c)) { |
| numDialable++; |
| } |
| } |
| return numDialable; |
| } |
| |
| /** |
| * Redact personally identifiable information for production users. |
| * If we are running in verbose mode, return the original string, |
| * and return "***" otherwise. |
| */ |
| public static String pii(Object pii) { |
| if (pii == null || VERBOSE) { |
| return String.valueOf(pii); |
| } |
| return "***"; |
| } |
| |
| private static String getPrefixFromObject(Object obj) { |
| return obj == null ? "<null>" : obj.getClass().getSimpleName(); |
| } |
| |
| private static String buildMessage(String prefix, String format, Object... args) { |
| // Incorporate thread ID and calling method into prefix |
| String sessionName = getSessionId(); |
| String sessionPostfix = TextUtils.isEmpty(sessionName) ? "" : ": " + sessionName; |
| |
| String msg; |
| try { |
| msg = (args == null || args.length == 0) ? format |
| : String.format(Locale.US, format, args); |
| } catch (IllegalFormatException ife) { |
| e(TAG, ife, "Log: IllegalFormatException: formatString='%s' numArgs=%d", format, |
| args.length); |
| msg = format + " (An error occurred while formatting the message.)"; |
| } |
| // If a lock was set, check if this thread holds that lock and output an emoji that lets |
| // the developer know whether a log message came from within the Telecom lock or not. |
| String isLocked = sLock != null ? (Thread.holdsLock(sLock) ? "\uD83D\uDD12" : "❗") : ""; |
| return String.format(Locale.US, "%s: %s%s%s", prefix, msg, sessionPostfix, isLocked); |
| } |
| |
| /** |
| * Generates an abbreviated version of the package name from a component. |
| * E.g. com.android.phone becomes cap |
| * @param componentName The component name to abbreviate. |
| * @return Abbreviation of empty string if component is null. |
| * @hide |
| */ |
| public static String getPackageAbbreviation(ComponentName componentName) { |
| if (componentName == null) { |
| return ""; |
| } |
| return getPackageAbbreviation(componentName.getPackageName()); |
| } |
| |
| /** |
| * Generates an abbreviated version of the package name. |
| * E.g. com.android.phone becomes cap |
| * @param packageName The packageName name to abbreviate. |
| * @return Abbreviation of empty string if package is null. |
| * @hide |
| */ |
| public static String getPackageAbbreviation(String packageName) { |
| if (packageName == null) { |
| return ""; |
| } |
| return Arrays.stream(packageName.split("\\.")) |
| .map(s -> s.length() == 0 ? "" : s.substring(0, 1)) |
| .collect(Collectors.joining("")); |
| } |
| } |