content: package allen.town.podcast.core.util.playback;
  
  import android.content.Context;
  import android.content.res.TypedArray;
  import android.graphics.Color;
  import androidx.annotation.ColorInt;
  import androidx.annotation.NonNull;
  import android.text.TextUtils;
  import android.util.Log;
  import android.util.TypedValue;
  
  import androidx.annotation.Nullable;
  import org.apache.commons.io.IOUtils;
  import org.jsoup.Jsoup;
  import org.jsoup.nodes.Document;
  import org.jsoup.nodes.Element;
  import org.jsoup.select.Elements;
  
  import java.io.IOException;
  import java.io.InputStream;
  import java.util.Locale;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  
  import code.name.monkey.appthemehelper.ThemeStore;
  import allen.town.podcast.core.R;
  import allen.town.podcast.core.util.Converter;
  
  /**
   * Connects chapter information and shownotes of a shownotesProvider, for example by making it possible to use the
   * shownotes to navigate to another position in the podcast or by highlighting certain parts of the shownotesProvider's
   * shownotes.
   * <p/>
   * A timeline object needs a shownotesProvider from which the chapter information
   * is retrieved and shownotes are generated.
   */
  public class Timeline {
      private static final String TAG = "Timeline";
  
      private static final Pattern TIMECODE_LINK_REGEX = Pattern.compile("allentown://focuspodcast-timecode/(\\d+)");
      private static final String TIMECODE_LINK = "<a class=\"timecode\" href=\"allentown://focuspodcast-timecode/%d\">%s</a>";
      private static final Pattern TIMECODE_REGEX = Pattern.compile("\\b((\\d+):)?(\\d+):(\\d{2})\\b");
      private static final Pattern LINE_BREAK_REGEX = Pattern.compile("<br */?>");
  
      private final String rawShownotes;
      private final String noShownotesLabel;
      private final int playableDuration;
      private final String webviewStyle;
  
      public Timeline(Context context, @Nullable String rawShownotes, int playableDuration) {
          this.rawShownotes = rawShownotes;
  
          noShownotesLabel = context.getString(R.string.no_shownotes_label);
          this.playableDuration = playableDuration;
          final String colorPrimary = colorAttrToHtml(context, android.R.attr.textColorPrimary);
          final String colorAccent = colorIntToHtml(context, ThemeStore.accentColor(context));
          final int margin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8,
                  context.getResources().getDisplayMetrics());
          String styleString = "";
          try {
              InputStream templateStream = context.getAssets().open("shownotesStyle.css");
              styleString = IOUtils.toString(templateStream, "UTF-8");
          } catch (IOException e) {
              e.printStackTrace();
          }
          webviewStyle = String.format(Locale.US, styleString, colorPrimary, colorAccent,colorAccent,
                  margin, margin, margin, margin);
      }
  
      private String colorAttrToHtml(Context context, int colorAttr) {
          TypedArray res = context.getTheme().obtainStyledAttributes(new int[]{colorAttr});
          @ColorInt int col = res.getColor(0, 0);
          final String color = colorToHtml(context,col);
          res.recycle();
          return color;
      }
  
      private String colorIntToHtml(Context context, @ColorInt int colorInt) {
          return colorToHtml(context, colorInt);
      }
  
      private String colorToHtml(Context context, @ColorInt int colorInt) {
          @ColorInt int col = colorInt;
          final String color = "rgba(" + Color.red(col) + "," + Color.green(col) + ","
                  + Color.blue(col) + "," + (Color.alpha(col) / 255.0) + ")";
          return color;
      }
  
      /**
       * Applies an app-specific CSS stylesheet and adds timecode links (optional).
       * <p/>
       * This method does NOT change the original shownotes string of the shownotesProvider object and it should
       * also not be changed by the caller.
       *
       * @return The processed HTML string.
       */
      @NonNull
      public String processShownotes() {
          String shownotes = rawShownotes;
  
          //加br是因为在pixel 4l Tiramisu上面显示不全
          if (TextUtils.isEmpty(shownotes)) {
              Log.d(TAG, "no shownotes");
              shownotes = "<html><head></head><body><p id='apNoShownotes'>" + noShownotesLabel + "</p><br/></body></html>";
          }
  
          // replace ASCII line breaks with HTML ones if shownotes don't contain HTML line breaks already
          if (!LINE_BREAK_REGEX.matcher(shownotes).find() && !shownotes.contains("<p>")) {
              shownotes = shownotes.replace("\n", "<br />");
          }
  
          Document document = Jsoup.parse(shownotes);
          document.head().appendElement("style").attr("type", "text/css").text(webviewStyle);
  
          // apply timecode links
          addTimecodes(document);
          return document.toString();
      }
  
      /**
       * Returns true if the given link is a timecode link.
       */
      public static boolean isTimecodeLink(String link) {
          return link != null && link.matches(TIMECODE_LINK_REGEX.pattern());
      }
  
      /**
       * Returns the time in milliseconds that is attached to this link or -1
       * if the link is no valid timecode link.
       */
      public static int getTimecodeLinkTime(String link) {
          if (isTimecodeLink(link)) {
              Matcher m = TIMECODE_LINK_REGEX.matcher(link);
  
              try {
                  if (m.find()) {
                      return Integer.parseInt(m.group(1));
                  }
              } catch (NumberFormatException e) {
                  e.printStackTrace();
              }
          }
          return -1;
      }
  
      private void addTimecodes(Document document) {
          Elements elementsWithTimeCodes = document.body().getElementsMatchingOwnText(TIMECODE_REGEX);
          Log.d(TAG, "find " + elementsWithTimeCodes.size() + " timecodes");
  
          if (elementsWithTimeCodes.size() == 0) {
              // No elements with timecodes
              return;
          }
          boolean useHourFormat = true;
  
          if (playableDuration != Integer.MAX_VALUE) {
  
              // We need to decide if we are going to treat short timecodes as HH:MM or MM:SS. To do
              // so we will parse all the short timecodes and see if they fit in the duration. If one
              // does not we will use MM:SS, otherwise all will be parsed as HH:MM.
              for (Element element : elementsWithTimeCodes) {
                  Matcher matcherForElement = TIMECODE_REGEX.matcher(element.html());
                  while (matcherForElement.find()) {
  
                      // We only want short timecodes right now.
                      if (matcherForElement.group(1) == null) {
                          int time = Converter.durationStringShortToMs(matcherForElement.group(0), true);
  
                          // If the parsed timecode is greater then the duration then we know we need to
                          // use the minute format so we are done.
                          if (time > playableDuration) {
                              useHourFormat = false;
                              break;
                          }
                      }
                  }
  
                  if (!useHourFormat) {
                      break;
                  }
              }
          }
  
          for (Element element : elementsWithTimeCodes) {
  
              Matcher matcherForElement = TIMECODE_REGEX.matcher(element.html());
              StringBuffer buffer = new StringBuffer();
  
              while (matcherForElement.find()) {
                  String group = matcherForElement.group(0);
  
                  int time = matcherForElement.group(1) != null
                                          ? Converter.durationStringLongToMs(group)
                                          : Converter.durationStringShortToMs(group, useHourFormat);
  
                  String replacementText = group;
                  if (time < playableDuration) {
                      replacementText = String.format(Locale.US, TIMECODE_LINK, time, group);
                  }
  
                  matcherForElement.appendReplacement(buffer, replacementText);
              }
  
              matcherForElement.appendTail(buffer);
              element.html(buffer.toString());
          }
      }
  }
  