content: /*
   * Copyright (C) 2017 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.view.textclassifier;
  
  import android.annotation.Nullable;
  import android.metrics.LogMaker;
  import android.util.ArrayMap;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.logging.MetricsLogger;
  import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
  import com.android.internal.util.Preconditions;
  
  import java.util.Locale;
  import java.util.Map;
  import java.util.Objects;
  import java.util.Random;
  import java.util.UUID;
  
  /**
   * A helper for logging calls to generateLinks.
   * @hide
   */
  @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
  public final class GenerateLinksLogger {
  
      private static final String LOG_TAG = "GenerateLinksLogger";
      private static final String ZERO = "0";
  
      private final MetricsLogger mMetricsLogger;
      private final Random mRng;
      private final int mSampleRate;
  
      /**
       * @param sampleRate the rate at which log events are written. (e.g. 100 means there is a 0.01
       *                   chance that a call to logGenerateLinks results in an event being written).
       *                   To write all events, pass 1.
       */
      public GenerateLinksLogger(int sampleRate) {
          mSampleRate = sampleRate;
          mRng = new Random(System.nanoTime());
          mMetricsLogger = new MetricsLogger();
      }
  
      @VisibleForTesting
      public GenerateLinksLogger(int sampleRate, MetricsLogger metricsLogger) {
          mSampleRate = sampleRate;
          mRng = new Random(System.nanoTime());
          mMetricsLogger = metricsLogger;
      }
  
      /** Logs statistics about a call to generateLinks. */
      public void logGenerateLinks(CharSequence text, TextLinks links, String callingPackageName,
              long latencyMs) {
          Preconditions.checkNotNull(text);
          Preconditions.checkNotNull(links);
          Preconditions.checkNotNull(callingPackageName);
          if (!shouldLog()) {
              return;
          }
  
          // Always populate the total stats, and per-entity stats for each entity type detected.
          final LinkifyStats totalStats = new LinkifyStats();
          final Map<String, LinkifyStats> perEntityTypeStats = new ArrayMap<>();
          for (TextLinks.TextLink link : links.getLinks()) {
              if (link.getEntityCount() == 0) continue;
              final String entityType = link.getEntity(0);
              if (entityType == null
                      || TextClassifier.TYPE_OTHER.equals(entityType)
                      || TextClassifier.TYPE_UNKNOWN.equals(entityType)) {
                  continue;
              }
              totalStats.countLink(link);
              perEntityTypeStats.computeIfAbsent(entityType, k -> new LinkifyStats()).countLink(link);
          }
  
          final String callId = UUID.randomUUID().toString();
          writeStats(callId, callingPackageName, null, totalStats, text, latencyMs);
          for (Map.Entry<String, LinkifyStats> entry : perEntityTypeStats.entrySet()) {
              writeStats(callId, callingPackageName, entry.getKey(), entry.getValue(), text,
                         latencyMs);
          }
      }
  
      /**
       * Returns whether this particular event should be logged.
       *
       * Sampling is used to reduce the amount of logging data generated.
       **/
      private boolean shouldLog() {
          if (mSampleRate <= 1) {
              return true;
          } else {
              return mRng.nextInt(mSampleRate) == 0;
          }
      }
  
      /** Writes a log event for the given stats. */
      private void writeStats(String callId, String callingPackageName, @Nullable String entityType,
                              LinkifyStats stats, CharSequence text, long latencyMs) {
          final LogMaker log = new LogMaker(MetricsEvent.TEXT_CLASSIFIER_GENERATE_LINKS)
                  .setPackageName(callingPackageName)
                  .addTaggedData(MetricsEvent.FIELD_LINKIFY_CALL_ID, callId)
                  .addTaggedData(MetricsEvent.FIELD_LINKIFY_NUM_LINKS, stats.mNumLinks)
                  .addTaggedData(MetricsEvent.FIELD_LINKIFY_LINK_LENGTH, stats.mNumLinksTextLength)
                  .addTaggedData(MetricsEvent.FIELD_LINKIFY_TEXT_LENGTH, text.length())
                  .addTaggedData(MetricsEvent.FIELD_LINKIFY_LATENCY, latencyMs);
          if (entityType != null) {
              log.addTaggedData(MetricsEvent.FIELD_LINKIFY_ENTITY_TYPE, entityType);
          }
          mMetricsLogger.write(log);
          debugLog(log);
      }
  
      private static void debugLog(LogMaker log) {
          if (!Log.ENABLE_FULL_LOGGING) {
              return;
          }
          final String callId = Objects.toString(
                  log.getTaggedData(MetricsEvent.FIELD_LINKIFY_CALL_ID), "");
          final String entityType = Objects.toString(
                  log.getTaggedData(MetricsEvent.FIELD_LINKIFY_ENTITY_TYPE), "ANY_ENTITY");
          final int numLinks = Integer.parseInt(
                  Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_NUM_LINKS), ZERO));
          final int linkLength = Integer.parseInt(
                  Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_LINK_LENGTH), ZERO));
          final int textLength = Integer.parseInt(
                  Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_TEXT_LENGTH), ZERO));
          final int latencyMs = Integer.parseInt(
                  Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_LATENCY), ZERO));
  
          Log.v(LOG_TAG,
                  String.format(Locale.US, "%s:%s %d links (%d/%d chars) %dms %s", callId, entityType,
                          numLinks, linkLength, textLength, latencyMs, log.getPackageName()));
      }
  
      /** Helper class for storing per-entity type statistics. */
      private static final class LinkifyStats {
          int mNumLinks;
          int mNumLinksTextLength;
  
          void countLink(TextLinks.TextLink link) {
              mNumLinks += 1;
              mNumLinksTextLength += link.getEnd() - link.getStart();
          }
      }
  }
  