content: /*
   * Copyright 2020 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 com.google.android.exoplayer2.extractor.jpeg;
  
  import androidx.annotation.Nullable;
  import com.google.android.exoplayer2.C;
  import com.google.android.exoplayer2.ParserException;
  import com.google.android.exoplayer2.util.Log;
  import com.google.android.exoplayer2.util.MimeTypes;
  import com.google.android.exoplayer2.util.XmlPullParserUtil;
  import com.google.common.collect.ImmutableList;
  import java.io.IOException;
  import java.io.StringReader;
  import java.util.List;
  import org.xmlpull.v1.XmlPullParser;
  import org.xmlpull.v1.XmlPullParserException;
  import org.xmlpull.v1.XmlPullParserFactory;
  
  /**
   * Parser for motion photo metadata, handling XMP following the Motion Photo V1 and Micro Video V1b
   * specifications.
   */
  /* package */ final class XmpMotionPhotoDescriptionParser {
  
    /**
     * Attempts to parse the specified XMP data describing the motion photo, returning the resulting
     * {@link MotionPhotoDescription} or {@code null} if it wasn't possible to derive a motion photo
     * description.
     *
     * @param xmpString A string of XML containing XMP motion photo metadata to attempt to parse.
     * @return The {@link MotionPhotoDescription}, or {@code null} if it wasn't possible to derive a
     *     motion photo description.
     * @throws IOException If an error occurs reading data from the stream.
     */
    @Nullable
    public static MotionPhotoDescription parse(String xmpString) throws IOException {
      try {
        return parseInternal(xmpString);
      } catch (XmlPullParserException | ParserException | NumberFormatException e) {
        Log.w(TAG, "Ignoring unexpected XMP metadata");
        return null;
      }
    }
  
    private static final String TAG = "MotionPhotoXmpParser";
  
    private static final String[] MOTION_PHOTO_ATTRIBUTE_NAMES =
        new String[] {
          "Camera:MotionPhoto", // Motion Photo V1
          "GCamera:MotionPhoto", // Motion Photo V1 (legacy element naming)
          "Camera:MicroVideo", // Micro Video V1b
          "GCamera:MicroVideo", // Micro Video V1b (legacy element naming)
        };
    private static final String[] DESCRIPTION_MOTION_PHOTO_PRESENTATION_TIMESTAMP_ATTRIBUTE_NAMES =
        new String[] {
          "Camera:MotionPhotoPresentationTimestampUs", // Motion Photo V1
          "GCamera:MotionPhotoPresentationTimestampUs", // Motion Photo V1 (legacy element naming)
          "Camera:MicroVideoPresentationTimestampUs", // Micro Video V1b
          "GCamera:MicroVideoPresentationTimestampUs", // Micro Video V1b (legacy element naming)
        };
    private static final String[] DESCRIPTION_MICRO_VIDEO_OFFSET_ATTRIBUTE_NAMES =
        new String[] {
          "Camera:MicroVideoOffset", // Micro Video V1b
          "GCamera:MicroVideoOffset", // Micro Video V1b (legacy element naming)
        };
  
    @Nullable
    private static MotionPhotoDescription parseInternal(String xmpString)
        throws XmlPullParserException, IOException {
      XmlPullParserFactory xmlPullParserFactory = XmlPullParserFactory.newInstance();
      XmlPullParser xpp = xmlPullParserFactory.newPullParser();
      xpp.setInput(new StringReader(xmpString));
      xpp.next();
      if (!XmlPullParserUtil.isStartTag(xpp, "x:xmpmeta")) {
        throw ParserException.createForMalformedContainer(
            "Couldn't find xmp metadata", /* cause= */ null);
      }
      long motionPhotoPresentationTimestampUs = C.TIME_UNSET;
      List<MotionPhotoDescription.ContainerItem> containerItems = ImmutableList.of();
      do {
        xpp.next();
        if (XmlPullParserUtil.isStartTag(xpp, "rdf:Description")) {
          if (!parseMotionPhotoFlagFromDescription(xpp)) {
            // The motion photo flag is not set, so the file should not be treated as a motion photo.
            return null;
          }
          motionPhotoPresentationTimestampUs =
              parseMotionPhotoPresentationTimestampUsFromDescription(xpp);
          containerItems = parseMicroVideoOffsetFromDescription(xpp);
        } else if (XmlPullParserUtil.isStartTag(xpp, "Container:Directory")) {
          containerItems = parseMotionPhotoV1Directory(xpp, "Container", "Item");
        } else if (XmlPullParserUtil.isStartTag(xpp, "GContainer:Directory")) {
          containerItems = parseMotionPhotoV1Directory(xpp, "GContainer", "GContainerItem");
        }
      } while (!XmlPullParserUtil.isEndTag(xpp, "x:xmpmeta"));
      if (containerItems.isEmpty()) {
        // No motion photo information was parsed.
        return null;
      }
      return new MotionPhotoDescription(motionPhotoPresentationTimestampUs, containerItems);
    }
  
    private static boolean parseMotionPhotoFlagFromDescription(XmlPullParser xpp) {
      for (String attributeName : MOTION_PHOTO_ATTRIBUTE_NAMES) {
        @Nullable String attributeValue = XmlPullParserUtil.getAttributeValue(xpp, attributeName);
        if (attributeValue != null) {
          int motionPhotoFlag = Integer.parseInt(attributeValue);
          return motionPhotoFlag == 1;
        }
      }
      return false;
    }
  
    private static long parseMotionPhotoPresentationTimestampUsFromDescription(XmlPullParser xpp) {
      for (String attributeName : DESCRIPTION_MOTION_PHOTO_PRESENTATION_TIMESTAMP_ATTRIBUTE_NAMES) {
        @Nullable String attributeValue = XmlPullParserUtil.getAttributeValue(xpp, attributeName);
        if (attributeValue != null) {
          long presentationTimestampUs = Long.parseLong(attributeValue);
          return presentationTimestampUs == -1 ? C.TIME_UNSET : presentationTimestampUs;
        }
      }
      return C.TIME_UNSET;
    }
  
    private static ImmutableList<MotionPhotoDescription.ContainerItem>
        parseMicroVideoOffsetFromDescription(XmlPullParser xpp) {
      // We store a new Motion Photo item list based on the MicroVideo offset, so that the same
      // representation is used for both specifications.
      for (String attributeName : DESCRIPTION_MICRO_VIDEO_OFFSET_ATTRIBUTE_NAMES) {
        @Nullable String attributeValue = XmlPullParserUtil.getAttributeValue(xpp, attributeName);
        if (attributeValue != null) {
          long microVideoOffset = Long.parseLong(attributeValue);
          return ImmutableList.of(
              new MotionPhotoDescription.ContainerItem(
                  MimeTypes.IMAGE_JPEG, "Primary", /* length= */ 0, /* padding= */ 0),
              new MotionPhotoDescription.ContainerItem(
                  MimeTypes.VIDEO_MP4,
                  "MotionPhoto",
                  /* length= */ microVideoOffset,
                  /* padding= */ 0));
        }
      }
      return ImmutableList.of();
    }
  
    private static ImmutableList<MotionPhotoDescription.ContainerItem> parseMotionPhotoV1Directory(
        XmlPullParser xpp, String containerNamespacePrefix, String itemNamespacePrefix)
        throws XmlPullParserException, IOException {
      ImmutableList.Builder<MotionPhotoDescription.ContainerItem> containerItems =
          ImmutableList.builder();
      String itemTagName = containerNamespacePrefix + ":Item";
      String directoryTagName = containerNamespacePrefix + ":Directory";
      do {
        xpp.next();
        if (XmlPullParserUtil.isStartTag(xpp, itemTagName)) {
          String mimeAttributeName = itemNamespacePrefix + ":Mime";
          String semanticAttributeName = itemNamespacePrefix + ":Semantic";
          String lengthAttributeName = itemNamespacePrefix + ":Length";
          String paddinghAttributeName = itemNamespacePrefix + ":Padding";
          @Nullable String mime = XmlPullParserUtil.getAttributeValue(xpp, mimeAttributeName);
          @Nullable String semantic = XmlPullParserUtil.getAttributeValue(xpp, semanticAttributeName);
          @Nullable String length = XmlPullParserUtil.getAttributeValue(xpp, lengthAttributeName);
          @Nullable String padding = XmlPullParserUtil.getAttributeValue(xpp, paddinghAttributeName);
          if (mime == null || semantic == null) {
            // Required values are missing.
            return ImmutableList.of();
          }
          containerItems.add(
              new MotionPhotoDescription.ContainerItem(
                  mime,
                  semantic,
                  length != null ? Long.parseLong(length) : 0,
                  padding != null ? Long.parseLong(padding) : 0));
        }
      } while (!XmlPullParserUtil.isEndTag(xpp, directoryTagName));
      return containerItems.build();
    }
  
    private XmpMotionPhotoDescriptionParser() {}
  }
  