diff options
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3')
14 files changed, 2078 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ApicFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ApicFrame.java new file mode 100644 index 0000000000..f151707e4b --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ApicFrame.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.Arrays; + +/** + * APIC (Attached Picture) ID3 frame. + */ +public final class ApicFrame extends Id3Frame { + + public static final String ID = "APIC"; + + public final String mimeType; + @Nullable public final String description; + public final int pictureType; + public final byte[] pictureData; + + public ApicFrame( + String mimeType, @Nullable String description, int pictureType, byte[] pictureData) { + super(ID); + this.mimeType = mimeType; + this.description = description; + this.pictureType = pictureType; + this.pictureData = pictureData; + } + + /* package */ ApicFrame(Parcel in) { + super(ID); + mimeType = castNonNull(in.readString()); + description = in.readString(); + pictureType = in.readInt(); + pictureData = castNonNull(in.createByteArray()); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ApicFrame other = (ApicFrame) obj; + return pictureType == other.pictureType && Util.areEqual(mimeType, other.mimeType) + && Util.areEqual(description, other.description) + && Arrays.equals(pictureData, other.pictureData); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + pictureType; + result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0); + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + Arrays.hashCode(pictureData); + return result; + } + + @Override + public String toString() { + return id + ": mimeType=" + mimeType + ", description=" + description; + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mimeType); + dest.writeString(description); + dest.writeInt(pictureType); + dest.writeByteArray(pictureData); + } + + public static final Parcelable.Creator<ApicFrame> CREATOR = new Parcelable.Creator<ApicFrame>() { + + @Override + public ApicFrame createFromParcel(Parcel in) { + return new ApicFrame(in); + } + + @Override + public ApicFrame[] newArray(int size) { + return new ApicFrame[size]; + } + + }; + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java new file mode 100644 index 0000000000..adc66ccdfe --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; +import java.util.Arrays; + +/** + * Binary ID3 frame. + */ +public final class BinaryFrame extends Id3Frame { + + public final byte[] data; + + public BinaryFrame(String id, byte[] data) { + super(id); + this.data = data; + } + + /* package */ BinaryFrame(Parcel in) { + super(castNonNull(in.readString())); + data = castNonNull(in.createByteArray()); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + BinaryFrame other = (BinaryFrame) obj; + return id.equals(other.id) && Arrays.equals(data, other.data); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + id.hashCode(); + result = 31 * result + Arrays.hashCode(data); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeByteArray(data); + } + + public static final Parcelable.Creator<BinaryFrame> CREATOR = + new Parcelable.Creator<BinaryFrame>() { + + @Override + public BinaryFrame createFromParcel(Parcel in) { + return new BinaryFrame(in); + } + + @Override + public BinaryFrame[] newArray(int size) { + return new BinaryFrame[size]; + } + + }; + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java new file mode 100644 index 0000000000..348781dddf --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java @@ -0,0 +1,145 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + +import android.os.Parcel; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.Arrays; + +/** + * Chapter information ID3 frame. + */ +public final class ChapterFrame extends Id3Frame { + + public static final String ID = "CHAP"; + + public final String chapterId; + public final int startTimeMs; + public final int endTimeMs; + /** + * The byte offset of the start of the chapter, or {@link C#POSITION_UNSET} if not set. + */ + public final long startOffset; + /** + * The byte offset of the end of the chapter, or {@link C#POSITION_UNSET} if not set. + */ + public final long endOffset; + private final Id3Frame[] subFrames; + + public ChapterFrame(String chapterId, int startTimeMs, int endTimeMs, long startOffset, + long endOffset, Id3Frame[] subFrames) { + super(ID); + this.chapterId = chapterId; + this.startTimeMs = startTimeMs; + this.endTimeMs = endTimeMs; + this.startOffset = startOffset; + this.endOffset = endOffset; + this.subFrames = subFrames; + } + + /* package */ ChapterFrame(Parcel in) { + super(ID); + this.chapterId = castNonNull(in.readString()); + this.startTimeMs = in.readInt(); + this.endTimeMs = in.readInt(); + this.startOffset = in.readLong(); + this.endOffset = in.readLong(); + int subFrameCount = in.readInt(); + subFrames = new Id3Frame[subFrameCount]; + for (int i = 0; i < subFrameCount; i++) { + subFrames[i] = in.readParcelable(Id3Frame.class.getClassLoader()); + } + } + + /** + * Returns the number of sub-frames. + */ + public int getSubFrameCount() { + return subFrames.length; + } + + /** + * Returns the sub-frame at {@code index}. + */ + public Id3Frame getSubFrame(int index) { + return subFrames[index]; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ChapterFrame other = (ChapterFrame) obj; + return startTimeMs == other.startTimeMs + && endTimeMs == other.endTimeMs + && startOffset == other.startOffset + && endOffset == other.endOffset + && Util.areEqual(chapterId, other.chapterId) + && Arrays.equals(subFrames, other.subFrames); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + startTimeMs; + result = 31 * result + endTimeMs; + result = 31 * result + (int) startOffset; + result = 31 * result + (int) endOffset; + result = 31 * result + (chapterId != null ? chapterId.hashCode() : 0); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(chapterId); + dest.writeInt(startTimeMs); + dest.writeInt(endTimeMs); + dest.writeLong(startOffset); + dest.writeLong(endOffset); + dest.writeInt(subFrames.length); + for (Id3Frame subFrame : subFrames) { + dest.writeParcelable(subFrame, 0); + } + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<ChapterFrame> CREATOR = new Creator<ChapterFrame>() { + + @Override + public ChapterFrame createFromParcel(Parcel in) { + return new ChapterFrame(in); + } + + @Override + public ChapterFrame[] newArray(int size) { + return new ChapterFrame[size]; + } + + }; + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java new file mode 100644 index 0000000000..9451151c16 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java @@ -0,0 +1,127 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + +import android.os.Parcel; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.Arrays; + +/** + * Chapter table of contents ID3 frame. + */ +public final class ChapterTocFrame extends Id3Frame { + + public static final String ID = "CTOC"; + + public final String elementId; + public final boolean isRoot; + public final boolean isOrdered; + public final String[] children; + private final Id3Frame[] subFrames; + + public ChapterTocFrame(String elementId, boolean isRoot, boolean isOrdered, String[] children, + Id3Frame[] subFrames) { + super(ID); + this.elementId = elementId; + this.isRoot = isRoot; + this.isOrdered = isOrdered; + this.children = children; + this.subFrames = subFrames; + } + + /* package */ + ChapterTocFrame(Parcel in) { + super(ID); + this.elementId = castNonNull(in.readString()); + this.isRoot = in.readByte() != 0; + this.isOrdered = in.readByte() != 0; + this.children = castNonNull(in.createStringArray()); + int subFrameCount = in.readInt(); + subFrames = new Id3Frame[subFrameCount]; + for (int i = 0; i < subFrameCount; i++) { + subFrames[i] = in.readParcelable(Id3Frame.class.getClassLoader()); + } + } + + /** + * Returns the number of sub-frames. + */ + public int getSubFrameCount() { + return subFrames.length; + } + + /** + * Returns the sub-frame at {@code index}. + */ + public Id3Frame getSubFrame(int index) { + return subFrames[index]; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ChapterTocFrame other = (ChapterTocFrame) obj; + return isRoot == other.isRoot + && isOrdered == other.isOrdered + && Util.areEqual(elementId, other.elementId) + && Arrays.equals(children, other.children) + && Arrays.equals(subFrames, other.subFrames); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (isRoot ? 1 : 0); + result = 31 * result + (isOrdered ? 1 : 0); + result = 31 * result + (elementId != null ? elementId.hashCode() : 0); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(elementId); + dest.writeByte((byte) (isRoot ? 1 : 0)); + dest.writeByte((byte) (isOrdered ? 1 : 0)); + dest.writeStringArray(children); + dest.writeInt(subFrames.length); + for (Id3Frame subFrame : subFrames) { + dest.writeParcelable(subFrame, 0); + } + } + + public static final Creator<ChapterTocFrame> CREATOR = new Creator<ChapterTocFrame>() { + + @Override + public ChapterTocFrame createFromParcel(Parcel in) { + return new ChapterTocFrame(in); + } + + @Override + public ChapterTocFrame[] newArray(int size) { + return new ChapterTocFrame[size]; + } + + }; + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/CommentFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/CommentFrame.java new file mode 100644 index 0000000000..98b8c79a96 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/CommentFrame.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** + * Comment ID3 frame. + */ +public final class CommentFrame extends Id3Frame { + + public static final String ID = "COMM"; + + public final String language; + public final String description; + public final String text; + + public CommentFrame(String language, String description, String text) { + super(ID); + this.language = language; + this.description = description; + this.text = text; + } + + /* package */ CommentFrame(Parcel in) { + super(ID); + language = castNonNull(in.readString()); + description = castNonNull(in.readString()); + text = castNonNull(in.readString()); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + CommentFrame other = (CommentFrame) obj; + return Util.areEqual(description, other.description) && Util.areEqual(language, other.language) + && Util.areEqual(text, other.text); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (language != null ? language.hashCode() : 0); + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + (text != null ? text.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return id + ": language=" + language + ", description=" + description; + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(language); + dest.writeString(text); + } + + public static final Parcelable.Creator<CommentFrame> CREATOR = + new Parcelable.Creator<CommentFrame>() { + + @Override + public CommentFrame createFromParcel(Parcel in) { + return new CommentFrame(in); + } + + @Override + public CommentFrame[] newArray(int size) { + return new CommentFrame[size]; + } + + }; + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/GeobFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/GeobFrame.java new file mode 100644 index 0000000000..58a208a76a --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/GeobFrame.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.Arrays; + +/** + * GEOB (General Encapsulated Object) ID3 frame. + */ +public final class GeobFrame extends Id3Frame { + + public static final String ID = "GEOB"; + + public final String mimeType; + public final String filename; + public final String description; + public final byte[] data; + + public GeobFrame(String mimeType, String filename, String description, byte[] data) { + super(ID); + this.mimeType = mimeType; + this.filename = filename; + this.description = description; + this.data = data; + } + + /* package */ GeobFrame(Parcel in) { + super(ID); + mimeType = castNonNull(in.readString()); + filename = castNonNull(in.readString()); + description = castNonNull(in.readString()); + data = castNonNull(in.createByteArray()); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + GeobFrame other = (GeobFrame) obj; + return Util.areEqual(mimeType, other.mimeType) && Util.areEqual(filename, other.filename) + && Util.areEqual(description, other.description) && Arrays.equals(data, other.data); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0); + result = 31 * result + (filename != null ? filename.hashCode() : 0); + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + Arrays.hashCode(data); + return result; + } + + @Override + public String toString() { + return id + + ": mimeType=" + + mimeType + + ", filename=" + + filename + + ", description=" + + description; + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mimeType); + dest.writeString(filename); + dest.writeString(description); + dest.writeByteArray(data); + } + + public static final Parcelable.Creator<GeobFrame> CREATOR = new Parcelable.Creator<GeobFrame>() { + + @Override + public GeobFrame createFromParcel(Parcel in) { + return new GeobFrame(in); + } + + @Override + public GeobFrame[] newArray(int size) { + return new GeobFrame[size]; + } + + }; + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java new file mode 100644 index 0000000000..36e004ed52 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -0,0 +1,842 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataDecoder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataInputBuffer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +/** + * Decodes ID3 tags. + */ +public final class Id3Decoder implements MetadataDecoder { + + /** + * A predicate for determining whether individual frames should be decoded. + */ + public interface FramePredicate { + + /** + * Returns whether a frame with the specified parameters should be decoded. + * + * @param majorVersion The major version of the ID3 tag. + * @param id0 The first byte of the frame ID. + * @param id1 The second byte of the frame ID. + * @param id2 The third byte of the frame ID. + * @param id3 The fourth byte of the frame ID. + * @return Whether the frame should be decoded. + */ + boolean evaluate(int majorVersion, int id0, int id1, int id2, int id3); + + } + + /** A predicate that indicates no frames should be decoded. */ + public static final FramePredicate NO_FRAMES_PREDICATE = + (majorVersion, id0, id1, id2, id3) -> false; + + private static final String TAG = "Id3Decoder"; + + /** The first three bytes of a well formed ID3 tag header. */ + public static final int ID3_TAG = 0x00494433; + /** + * Length of an ID3 tag header. + */ + public static final int ID3_HEADER_LENGTH = 10; + + private static final int FRAME_FLAG_V3_IS_COMPRESSED = 0x0080; + private static final int FRAME_FLAG_V3_IS_ENCRYPTED = 0x0040; + private static final int FRAME_FLAG_V3_HAS_GROUP_IDENTIFIER = 0x0020; + private static final int FRAME_FLAG_V4_IS_COMPRESSED = 0x0008; + private static final int FRAME_FLAG_V4_IS_ENCRYPTED = 0x0004; + private static final int FRAME_FLAG_V4_HAS_GROUP_IDENTIFIER = 0x0040; + private static final int FRAME_FLAG_V4_IS_UNSYNCHRONIZED = 0x0002; + private static final int FRAME_FLAG_V4_HAS_DATA_LENGTH = 0x0001; + + private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0; + private static final int ID3_TEXT_ENCODING_UTF_16 = 1; + private static final int ID3_TEXT_ENCODING_UTF_16BE = 2; + private static final int ID3_TEXT_ENCODING_UTF_8 = 3; + + @Nullable private final FramePredicate framePredicate; + + public Id3Decoder() { + this(null); + } + + /** + * @param framePredicate Determines which frames are decoded. May be null to decode all frames. + */ + public Id3Decoder(@Nullable FramePredicate framePredicate) { + this.framePredicate = framePredicate; + } + + @SuppressWarnings("ByteBufferBackingArray") + @Override + @Nullable + public Metadata decode(MetadataInputBuffer inputBuffer) { + ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); + return decode(buffer.array(), buffer.limit()); + } + + /** + * Decodes ID3 tags. + * + * @param data The bytes to decode ID3 tags from. + * @param size Amount of bytes in {@code data} to read. + * @return A {@link Metadata} object containing the decoded ID3 tags, or null if the data could + * not be decoded. + */ + @Nullable + public Metadata decode(byte[] data, int size) { + List<Id3Frame> id3Frames = new ArrayList<>(); + ParsableByteArray id3Data = new ParsableByteArray(data, size); + + Id3Header id3Header = decodeHeader(id3Data); + if (id3Header == null) { + return null; + } + + int startPosition = id3Data.getPosition(); + int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10; + int framesSize = id3Header.framesSize; + if (id3Header.isUnsynchronized) { + framesSize = removeUnsynchronization(id3Data, id3Header.framesSize); + } + id3Data.setLimit(startPosition + framesSize); + + boolean unsignedIntFrameSizeHack = false; + if (!validateFrames(id3Data, id3Header.majorVersion, frameHeaderSize, false)) { + if (id3Header.majorVersion == 4 && validateFrames(id3Data, 4, frameHeaderSize, true)) { + unsignedIntFrameSizeHack = true; + } else { + Log.w(TAG, "Failed to validate ID3 tag with majorVersion=" + id3Header.majorVersion); + return null; + } + } + + while (id3Data.bytesLeft() >= frameHeaderSize) { + Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data, unsignedIntFrameSizeHack, + frameHeaderSize, framePredicate); + if (frame != null) { + id3Frames.add(frame); + } + } + + return new Metadata(id3Frames); + } + + /** + * @param data A {@link ParsableByteArray} from which the header should be read. + * @return The parsed header, or null if the ID3 tag is unsupported. + */ + @Nullable + private static Id3Header decodeHeader(ParsableByteArray data) { + if (data.bytesLeft() < ID3_HEADER_LENGTH) { + Log.w(TAG, "Data too short to be an ID3 tag"); + return null; + } + + int id = data.readUnsignedInt24(); + if (id != ID3_TAG) { + Log.w(TAG, "Unexpected first three bytes of ID3 tag header: 0x" + String.format("%06X", id)); + return null; + } + + int majorVersion = data.readUnsignedByte(); + data.skipBytes(1); // Skip minor version. + int flags = data.readUnsignedByte(); + int framesSize = data.readSynchSafeInt(); + + if (majorVersion == 2) { + boolean isCompressed = (flags & 0x40) != 0; + if (isCompressed) { + Log.w(TAG, "Skipped ID3 tag with majorVersion=2 and undefined compression scheme"); + return null; + } + } else if (majorVersion == 3) { + boolean hasExtendedHeader = (flags & 0x40) != 0; + if (hasExtendedHeader) { + int extendedHeaderSize = data.readInt(); // Size excluding size field. + data.skipBytes(extendedHeaderSize); + framesSize -= (extendedHeaderSize + 4); + } + } else if (majorVersion == 4) { + boolean hasExtendedHeader = (flags & 0x40) != 0; + if (hasExtendedHeader) { + int extendedHeaderSize = data.readSynchSafeInt(); // Size including size field. + data.skipBytes(extendedHeaderSize - 4); + framesSize -= extendedHeaderSize; + } + boolean hasFooter = (flags & 0x10) != 0; + if (hasFooter) { + framesSize -= 10; + } + } else { + Log.w(TAG, "Skipped ID3 tag with unsupported majorVersion=" + majorVersion); + return null; + } + + // isUnsynchronized is advisory only in version 4. Frame level flags are used instead. + boolean isUnsynchronized = majorVersion < 4 && (flags & 0x80) != 0; + return new Id3Header(majorVersion, isUnsynchronized, framesSize); + } + + private static boolean validateFrames(ParsableByteArray id3Data, int majorVersion, + int frameHeaderSize, boolean unsignedIntFrameSizeHack) { + int startPosition = id3Data.getPosition(); + try { + while (id3Data.bytesLeft() >= frameHeaderSize) { + // Read the next frame header. + int id; + long frameSize; + int flags; + if (majorVersion >= 3) { + id = id3Data.readInt(); + frameSize = id3Data.readUnsignedInt(); + flags = id3Data.readUnsignedShort(); + } else { + id = id3Data.readUnsignedInt24(); + frameSize = id3Data.readUnsignedInt24(); + flags = 0; + } + // Validate the frame header and skip to the next one. + if (id == 0 && frameSize == 0 && flags == 0) { + // We've reached zero padding after the end of the final frame. + return true; + } else { + if (majorVersion == 4 && !unsignedIntFrameSizeHack) { + // Parse the data size as a synchsafe integer, as per the spec. + if ((frameSize & 0x808080L) != 0) { + return false; + } + frameSize = (frameSize & 0xFF) | (((frameSize >> 8) & 0xFF) << 7) + | (((frameSize >> 16) & 0xFF) << 14) | (((frameSize >> 24) & 0xFF) << 21); + } + boolean hasGroupIdentifier = false; + boolean hasDataLength = false; + if (majorVersion == 4) { + hasGroupIdentifier = (flags & FRAME_FLAG_V4_HAS_GROUP_IDENTIFIER) != 0; + hasDataLength = (flags & FRAME_FLAG_V4_HAS_DATA_LENGTH) != 0; + } else if (majorVersion == 3) { + hasGroupIdentifier = (flags & FRAME_FLAG_V3_HAS_GROUP_IDENTIFIER) != 0; + // A V3 frame has data length if and only if it's compressed. + hasDataLength = (flags & FRAME_FLAG_V3_IS_COMPRESSED) != 0; + } + int minimumFrameSize = 0; + if (hasGroupIdentifier) { + minimumFrameSize++; + } + if (hasDataLength) { + minimumFrameSize += 4; + } + if (frameSize < minimumFrameSize) { + return false; + } + if (id3Data.bytesLeft() < frameSize) { + return false; + } + id3Data.skipBytes((int) frameSize); // flags + } + } + return true; + } finally { + id3Data.setPosition(startPosition); + } + } + + @Nullable + private static Id3Frame decodeFrame( + int majorVersion, + ParsableByteArray id3Data, + boolean unsignedIntFrameSizeHack, + int frameHeaderSize, + @Nullable FramePredicate framePredicate) { + int frameId0 = id3Data.readUnsignedByte(); + int frameId1 = id3Data.readUnsignedByte(); + int frameId2 = id3Data.readUnsignedByte(); + int frameId3 = majorVersion >= 3 ? id3Data.readUnsignedByte() : 0; + + int frameSize; + if (majorVersion == 4) { + frameSize = id3Data.readUnsignedIntToInt(); + if (!unsignedIntFrameSizeHack) { + frameSize = (frameSize & 0xFF) | (((frameSize >> 8) & 0xFF) << 7) + | (((frameSize >> 16) & 0xFF) << 14) | (((frameSize >> 24) & 0xFF) << 21); + } + } else if (majorVersion == 3) { + frameSize = id3Data.readUnsignedIntToInt(); + } else /* id3Header.majorVersion == 2 */ { + frameSize = id3Data.readUnsignedInt24(); + } + + int flags = majorVersion >= 3 ? id3Data.readUnsignedShort() : 0; + if (frameId0 == 0 && frameId1 == 0 && frameId2 == 0 && frameId3 == 0 && frameSize == 0 + && flags == 0) { + // We must be reading zero padding at the end of the tag. + id3Data.setPosition(id3Data.limit()); + return null; + } + + int nextFramePosition = id3Data.getPosition() + frameSize; + if (nextFramePosition > id3Data.limit()) { + Log.w(TAG, "Frame size exceeds remaining tag data"); + id3Data.setPosition(id3Data.limit()); + return null; + } + + if (framePredicate != null + && !framePredicate.evaluate(majorVersion, frameId0, frameId1, frameId2, frameId3)) { + // Filtered by the predicate. + id3Data.setPosition(nextFramePosition); + return null; + } + + // Frame flags. + boolean isCompressed = false; + boolean isEncrypted = false; + boolean isUnsynchronized = false; + boolean hasDataLength = false; + boolean hasGroupIdentifier = false; + if (majorVersion == 3) { + isCompressed = (flags & FRAME_FLAG_V3_IS_COMPRESSED) != 0; + isEncrypted = (flags & FRAME_FLAG_V3_IS_ENCRYPTED) != 0; + hasGroupIdentifier = (flags & FRAME_FLAG_V3_HAS_GROUP_IDENTIFIER) != 0; + // A V3 frame has data length if and only if it's compressed. + hasDataLength = isCompressed; + } else if (majorVersion == 4) { + hasGroupIdentifier = (flags & FRAME_FLAG_V4_HAS_GROUP_IDENTIFIER) != 0; + isCompressed = (flags & FRAME_FLAG_V4_IS_COMPRESSED) != 0; + isEncrypted = (flags & FRAME_FLAG_V4_IS_ENCRYPTED) != 0; + isUnsynchronized = (flags & FRAME_FLAG_V4_IS_UNSYNCHRONIZED) != 0; + hasDataLength = (flags & FRAME_FLAG_V4_HAS_DATA_LENGTH) != 0; + } + + if (isCompressed || isEncrypted) { + Log.w(TAG, "Skipping unsupported compressed or encrypted frame"); + id3Data.setPosition(nextFramePosition); + return null; + } + + if (hasGroupIdentifier) { + frameSize--; + id3Data.skipBytes(1); + } + if (hasDataLength) { + frameSize -= 4; + id3Data.skipBytes(4); + } + if (isUnsynchronized) { + frameSize = removeUnsynchronization(id3Data, frameSize); + } + + try { + Id3Frame frame; + if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' + && (majorVersion == 2 || frameId3 == 'X')) { + frame = decodeTxxxFrame(id3Data, frameSize); + } else if (frameId0 == 'T') { + String id = getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3); + frame = decodeTextInformationFrame(id3Data, frameSize, id); + } else if (frameId0 == 'W' && frameId1 == 'X' && frameId2 == 'X' + && (majorVersion == 2 || frameId3 == 'X')) { + frame = decodeWxxxFrame(id3Data, frameSize); + } else if (frameId0 == 'W') { + String id = getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3); + frame = decodeUrlLinkFrame(id3Data, frameSize, id); + } else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') { + frame = decodePrivFrame(id3Data, frameSize); + } else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' + && (frameId3 == 'B' || majorVersion == 2)) { + frame = decodeGeobFrame(id3Data, frameSize); + } else if (majorVersion == 2 ? (frameId0 == 'P' && frameId1 == 'I' && frameId2 == 'C') + : (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C')) { + frame = decodeApicFrame(id3Data, frameSize, majorVersion); + } else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' + && (frameId3 == 'M' || majorVersion == 2)) { + frame = decodeCommentFrame(id3Data, frameSize); + } else if (frameId0 == 'C' && frameId1 == 'H' && frameId2 == 'A' && frameId3 == 'P') { + frame = decodeChapterFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack, + frameHeaderSize, framePredicate); + } else if (frameId0 == 'C' && frameId1 == 'T' && frameId2 == 'O' && frameId3 == 'C') { + frame = decodeChapterTOCFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack, + frameHeaderSize, framePredicate); + } else if (frameId0 == 'M' && frameId1 == 'L' && frameId2 == 'L' && frameId3 == 'T') { + frame = decodeMlltFrame(id3Data, frameSize); + } else { + String id = getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3); + frame = decodeBinaryFrame(id3Data, frameSize, id); + } + if (frame == null) { + Log.w(TAG, "Failed to decode frame: id=" + + getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3) + ", frameSize=" + + frameSize); + } + return frame; + } catch (UnsupportedEncodingException e) { + Log.w(TAG, "Unsupported character encoding"); + return null; + } finally { + id3Data.setPosition(nextFramePosition); + } + } + + @Nullable + private static TextInformationFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize) + throws UnsupportedEncodingException { + if (frameSize < 1) { + // Frame is malformed. + return null; + } + + int encoding = id3Data.readUnsignedByte(); + String charset = getCharsetName(encoding); + + byte[] data = new byte[frameSize - 1]; + id3Data.readBytes(data, 0, frameSize - 1); + + int descriptionEndIndex = indexOfEos(data, 0, encoding); + String description = new String(data, 0, descriptionEndIndex, charset); + + int valueStartIndex = descriptionEndIndex + delimiterLength(encoding); + int valueEndIndex = indexOfEos(data, valueStartIndex, encoding); + String value = decodeStringIfValid(data, valueStartIndex, valueEndIndex, charset); + + return new TextInformationFrame("TXXX", description, value); + } + + @Nullable + private static TextInformationFrame decodeTextInformationFrame( + ParsableByteArray id3Data, int frameSize, String id) throws UnsupportedEncodingException { + if (frameSize < 1) { + // Frame is malformed. + return null; + } + + int encoding = id3Data.readUnsignedByte(); + String charset = getCharsetName(encoding); + + byte[] data = new byte[frameSize - 1]; + id3Data.readBytes(data, 0, frameSize - 1); + + int valueEndIndex = indexOfEos(data, 0, encoding); + String value = new String(data, 0, valueEndIndex, charset); + + return new TextInformationFrame(id, null, value); + } + + @Nullable + private static UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, int frameSize) + throws UnsupportedEncodingException { + if (frameSize < 1) { + // Frame is malformed. + return null; + } + + int encoding = id3Data.readUnsignedByte(); + String charset = getCharsetName(encoding); + + byte[] data = new byte[frameSize - 1]; + id3Data.readBytes(data, 0, frameSize - 1); + + int descriptionEndIndex = indexOfEos(data, 0, encoding); + String description = new String(data, 0, descriptionEndIndex, charset); + + int urlStartIndex = descriptionEndIndex + delimiterLength(encoding); + int urlEndIndex = indexOfZeroByte(data, urlStartIndex); + String url = decodeStringIfValid(data, urlStartIndex, urlEndIndex, "ISO-8859-1"); + + return new UrlLinkFrame("WXXX", description, url); + } + + private static UrlLinkFrame decodeUrlLinkFrame(ParsableByteArray id3Data, int frameSize, + String id) throws UnsupportedEncodingException { + byte[] data = new byte[frameSize]; + id3Data.readBytes(data, 0, frameSize); + + int urlEndIndex = indexOfZeroByte(data, 0); + String url = new String(data, 0, urlEndIndex, "ISO-8859-1"); + + return new UrlLinkFrame(id, null, url); + } + + private static PrivFrame decodePrivFrame(ParsableByteArray id3Data, int frameSize) + throws UnsupportedEncodingException { + byte[] data = new byte[frameSize]; + id3Data.readBytes(data, 0, frameSize); + + int ownerEndIndex = indexOfZeroByte(data, 0); + String owner = new String(data, 0, ownerEndIndex, "ISO-8859-1"); + + int privateDataStartIndex = ownerEndIndex + 1; + byte[] privateData = copyOfRangeIfValid(data, privateDataStartIndex, data.length); + + return new PrivFrame(owner, privateData); + } + + private static GeobFrame decodeGeobFrame(ParsableByteArray id3Data, int frameSize) + throws UnsupportedEncodingException { + int encoding = id3Data.readUnsignedByte(); + String charset = getCharsetName(encoding); + + byte[] data = new byte[frameSize - 1]; + id3Data.readBytes(data, 0, frameSize - 1); + + int mimeTypeEndIndex = indexOfZeroByte(data, 0); + String mimeType = new String(data, 0, mimeTypeEndIndex, "ISO-8859-1"); + + int filenameStartIndex = mimeTypeEndIndex + 1; + int filenameEndIndex = indexOfEos(data, filenameStartIndex, encoding); + String filename = decodeStringIfValid(data, filenameStartIndex, filenameEndIndex, charset); + + int descriptionStartIndex = filenameEndIndex + delimiterLength(encoding); + int descriptionEndIndex = indexOfEos(data, descriptionStartIndex, encoding); + String description = + decodeStringIfValid(data, descriptionStartIndex, descriptionEndIndex, charset); + + int objectDataStartIndex = descriptionEndIndex + delimiterLength(encoding); + byte[] objectData = copyOfRangeIfValid(data, objectDataStartIndex, data.length); + + return new GeobFrame(mimeType, filename, description, objectData); + } + + private static ApicFrame decodeApicFrame(ParsableByteArray id3Data, int frameSize, + int majorVersion) throws UnsupportedEncodingException { + int encoding = id3Data.readUnsignedByte(); + String charset = getCharsetName(encoding); + + byte[] data = new byte[frameSize - 1]; + id3Data.readBytes(data, 0, frameSize - 1); + + String mimeType; + int mimeTypeEndIndex; + if (majorVersion == 2) { + mimeTypeEndIndex = 2; + mimeType = "image/" + Util.toLowerInvariant(new String(data, 0, 3, "ISO-8859-1")); + if ("image/jpg".equals(mimeType)) { + mimeType = "image/jpeg"; + } + } else { + mimeTypeEndIndex = indexOfZeroByte(data, 0); + mimeType = Util.toLowerInvariant(new String(data, 0, mimeTypeEndIndex, "ISO-8859-1")); + if (mimeType.indexOf('/') == -1) { + mimeType = "image/" + mimeType; + } + } + + int pictureType = data[mimeTypeEndIndex + 1] & 0xFF; + + int descriptionStartIndex = mimeTypeEndIndex + 2; + int descriptionEndIndex = indexOfEos(data, descriptionStartIndex, encoding); + String description = new String(data, descriptionStartIndex, + descriptionEndIndex - descriptionStartIndex, charset); + + int pictureDataStartIndex = descriptionEndIndex + delimiterLength(encoding); + byte[] pictureData = copyOfRangeIfValid(data, pictureDataStartIndex, data.length); + + return new ApicFrame(mimeType, description, pictureType, pictureData); + } + + @Nullable + private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize) + throws UnsupportedEncodingException { + if (frameSize < 4) { + // Frame is malformed. + return null; + } + + int encoding = id3Data.readUnsignedByte(); + String charset = getCharsetName(encoding); + + byte[] data = new byte[3]; + id3Data.readBytes(data, 0, 3); + String language = new String(data, 0, 3); + + data = new byte[frameSize - 4]; + id3Data.readBytes(data, 0, frameSize - 4); + + int descriptionEndIndex = indexOfEos(data, 0, encoding); + String description = new String(data, 0, descriptionEndIndex, charset); + + int textStartIndex = descriptionEndIndex + delimiterLength(encoding); + int textEndIndex = indexOfEos(data, textStartIndex, encoding); + String text = decodeStringIfValid(data, textStartIndex, textEndIndex, charset); + + return new CommentFrame(language, description, text); + } + + private static ChapterFrame decodeChapterFrame( + ParsableByteArray id3Data, + int frameSize, + int majorVersion, + boolean unsignedIntFrameSizeHack, + int frameHeaderSize, + @Nullable FramePredicate framePredicate) + throws UnsupportedEncodingException { + int framePosition = id3Data.getPosition(); + int chapterIdEndIndex = indexOfZeroByte(id3Data.data, framePosition); + String chapterId = new String(id3Data.data, framePosition, chapterIdEndIndex - framePosition, + "ISO-8859-1"); + id3Data.setPosition(chapterIdEndIndex + 1); + + int startTime = id3Data.readInt(); + int endTime = id3Data.readInt(); + long startOffset = id3Data.readUnsignedInt(); + if (startOffset == 0xFFFFFFFFL) { + startOffset = C.POSITION_UNSET; + } + long endOffset = id3Data.readUnsignedInt(); + if (endOffset == 0xFFFFFFFFL) { + endOffset = C.POSITION_UNSET; + } + + ArrayList<Id3Frame> subFrames = new ArrayList<>(); + int limit = framePosition + frameSize; + while (id3Data.getPosition() < limit) { + Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack, + frameHeaderSize, framePredicate); + if (frame != null) { + subFrames.add(frame); + } + } + + Id3Frame[] subFrameArray = new Id3Frame[subFrames.size()]; + subFrames.toArray(subFrameArray); + return new ChapterFrame(chapterId, startTime, endTime, startOffset, endOffset, subFrameArray); + } + + private static ChapterTocFrame decodeChapterTOCFrame( + ParsableByteArray id3Data, + int frameSize, + int majorVersion, + boolean unsignedIntFrameSizeHack, + int frameHeaderSize, + @Nullable FramePredicate framePredicate) + throws UnsupportedEncodingException { + int framePosition = id3Data.getPosition(); + int elementIdEndIndex = indexOfZeroByte(id3Data.data, framePosition); + String elementId = new String(id3Data.data, framePosition, elementIdEndIndex - framePosition, + "ISO-8859-1"); + id3Data.setPosition(elementIdEndIndex + 1); + + int ctocFlags = id3Data.readUnsignedByte(); + boolean isRoot = (ctocFlags & 0x0002) != 0; + boolean isOrdered = (ctocFlags & 0x0001) != 0; + + int childCount = id3Data.readUnsignedByte(); + String[] children = new String[childCount]; + for (int i = 0; i < childCount; i++) { + int startIndex = id3Data.getPosition(); + int endIndex = indexOfZeroByte(id3Data.data, startIndex); + children[i] = new String(id3Data.data, startIndex, endIndex - startIndex, "ISO-8859-1"); + id3Data.setPosition(endIndex + 1); + } + + ArrayList<Id3Frame> subFrames = new ArrayList<>(); + int limit = framePosition + frameSize; + while (id3Data.getPosition() < limit) { + Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack, + frameHeaderSize, framePredicate); + if (frame != null) { + subFrames.add(frame); + } + } + + Id3Frame[] subFrameArray = new Id3Frame[subFrames.size()]; + subFrames.toArray(subFrameArray); + return new ChapterTocFrame(elementId, isRoot, isOrdered, children, subFrameArray); + } + + private static MlltFrame decodeMlltFrame(ParsableByteArray id3Data, int frameSize) { + // See ID3v2.4.0 native frames subsection 4.6. + int mpegFramesBetweenReference = id3Data.readUnsignedShort(); + int bytesBetweenReference = id3Data.readUnsignedInt24(); + int millisecondsBetweenReference = id3Data.readUnsignedInt24(); + int bitsForBytesDeviation = id3Data.readUnsignedByte(); + int bitsForMillisecondsDeviation = id3Data.readUnsignedByte(); + + ParsableBitArray references = new ParsableBitArray(); + references.reset(id3Data); + int referencesBits = 8 * (frameSize - 10); + int bitsPerReference = bitsForBytesDeviation + bitsForMillisecondsDeviation; + int referencesCount = referencesBits / bitsPerReference; + int[] bytesDeviations = new int[referencesCount]; + int[] millisecondsDeviations = new int[referencesCount]; + for (int i = 0; i < referencesCount; i++) { + int bytesDeviation = references.readBits(bitsForBytesDeviation); + int millisecondsDeviation = references.readBits(bitsForMillisecondsDeviation); + bytesDeviations[i] = bytesDeviation; + millisecondsDeviations[i] = millisecondsDeviation; + } + + return new MlltFrame( + mpegFramesBetweenReference, + bytesBetweenReference, + millisecondsBetweenReference, + bytesDeviations, + millisecondsDeviations); + } + + private static BinaryFrame decodeBinaryFrame(ParsableByteArray id3Data, int frameSize, + String id) { + byte[] frame = new byte[frameSize]; + id3Data.readBytes(frame, 0, frameSize); + + return new BinaryFrame(id, frame); + } + + /** + * Performs in-place removal of unsynchronization for {@code length} bytes starting from + * {@link ParsableByteArray#getPosition()} + * + * @param data Contains the data to be processed. + * @param length The length of the data to be processed. + * @return The length of the data after processing. + */ + private static int removeUnsynchronization(ParsableByteArray data, int length) { + byte[] bytes = data.data; + int startPosition = data.getPosition(); + for (int i = startPosition; i + 1 < startPosition + length; i++) { + if ((bytes[i] & 0xFF) == 0xFF && bytes[i + 1] == 0x00) { + int relativePosition = i - startPosition; + System.arraycopy(bytes, i + 2, bytes, i + 1, length - relativePosition - 2); + length--; + } + } + return length; + } + + /** + * Maps encoding byte from ID3v2 frame to a Charset. + * + * @param encodingByte The value of encoding byte from ID3v2 frame. + * @return Charset name. + */ + private static String getCharsetName(int encodingByte) { + switch (encodingByte) { + case ID3_TEXT_ENCODING_UTF_16: + return "UTF-16"; + case ID3_TEXT_ENCODING_UTF_16BE: + return "UTF-16BE"; + case ID3_TEXT_ENCODING_UTF_8: + return "UTF-8"; + case ID3_TEXT_ENCODING_ISO_8859_1: + default: + return "ISO-8859-1"; + } + } + + private static String getFrameId(int majorVersion, int frameId0, int frameId1, int frameId2, + int frameId3) { + return majorVersion == 2 ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2) + : String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3); + } + + private static int indexOfEos(byte[] data, int fromIndex, int encoding) { + int terminationPos = indexOfZeroByte(data, fromIndex); + + // For single byte encoding charsets, we're done. + if (encoding == ID3_TEXT_ENCODING_ISO_8859_1 || encoding == ID3_TEXT_ENCODING_UTF_8) { + return terminationPos; + } + + // Otherwise ensure an even index and look for a second zero byte. + while (terminationPos < data.length - 1) { + if (terminationPos % 2 == 0 && data[terminationPos + 1] == (byte) 0) { + return terminationPos; + } + terminationPos = indexOfZeroByte(data, terminationPos + 1); + } + + return data.length; + } + + private static int indexOfZeroByte(byte[] data, int fromIndex) { + for (int i = fromIndex; i < data.length; i++) { + if (data[i] == (byte) 0) { + return i; + } + } + return data.length; + } + + private static int delimiterLength(int encodingByte) { + return (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || encodingByte == ID3_TEXT_ENCODING_UTF_8) + ? 1 : 2; + } + + /** + * Copies the specified range of an array, or returns a zero length array if the range is invalid. + * + * @param data The array from which to copy. + * @param from The start of the range to copy (inclusive). + * @param to The end of the range to copy (exclusive). + * @return The copied data, or a zero length array if the range is invalid. + */ + private static byte[] copyOfRangeIfValid(byte[] data, int from, int to) { + if (to <= from) { + // Invalid or zero length range. + return Util.EMPTY_BYTE_ARRAY; + } + return Arrays.copyOfRange(data, from, to); + } + + /** + * Returns a string obtained by decoding the specified range of {@code data} using the specified + * {@code charsetName}. An empty string is returned if the range is invalid. + * + * @param data The array from which to decode the string. + * @param from The start of the range. + * @param to The end of the range (exclusive). + * @param charsetName The name of the Charset to use. + * @return The decoded string, or an empty string if the range is invalid. + * @throws UnsupportedEncodingException If the Charset is not supported. + */ + private static String decodeStringIfValid(byte[] data, int from, int to, String charsetName) + throws UnsupportedEncodingException { + if (to <= from || to > data.length) { + return ""; + } + return new String(data, from, to - from, charsetName); + } + + private static final class Id3Header { + + private final int majorVersion; + private final boolean isUnsynchronized; + private final int framesSize; + + public Id3Header(int majorVersion, boolean isUnsynchronized, int framesSize) { + this.majorVersion = majorVersion; + this.isUnsynchronized = isUnsynchronized; + this.framesSize = framesSize; + } + + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Frame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Frame.java new file mode 100644 index 0000000000..f96b5e752c --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Frame.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; + +/** + * Base class for ID3 frames. + */ +public abstract class Id3Frame implements Metadata.Entry { + + /** + * The frame ID. + */ + public final String id; + + public Id3Frame(String id) { + this.id = id; + } + + @Override + public String toString() { + return id; + } + + @Override + public int describeContents() { + return 0; + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/InternalFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/InternalFrame.java new file mode 100644 index 0000000000..ab8ccff343 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/InternalFrame.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + +import android.os.Parcel; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** Internal ID3 frame that is intended for use by the player. */ +public final class InternalFrame extends Id3Frame { + + public static final String ID = "----"; + + public final String domain; + public final String description; + public final String text; + + public InternalFrame(String domain, String description, String text) { + super(ID); + this.domain = domain; + this.description = description; + this.text = text; + } + + /* package */ InternalFrame(Parcel in) { + super(ID); + domain = castNonNull(in.readString()); + description = castNonNull(in.readString()); + text = castNonNull(in.readString()); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + InternalFrame other = (InternalFrame) obj; + return Util.areEqual(description, other.description) + && Util.areEqual(domain, other.domain) + && Util.areEqual(text, other.text); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (domain != null ? domain.hashCode() : 0); + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + (text != null ? text.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return id + ": domain=" + domain + ", description=" + description; + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(domain); + dest.writeString(text); + } + + public static final Creator<InternalFrame> CREATOR = + new Creator<InternalFrame>() { + + @Override + public InternalFrame createFromParcel(Parcel in) { + return new InternalFrame(in); + } + + @Override + public InternalFrame[] newArray(int size) { + return new InternalFrame[size]; + } + }; +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/MlltFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/MlltFrame.java new file mode 100644 index 0000000000..441235d7c9 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/MlltFrame.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; + +import android.os.Parcel; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.Arrays; + +/** MPEG location lookup table frame. */ +public final class MlltFrame extends Id3Frame { + + public static final String ID = "MLLT"; + + public final int mpegFramesBetweenReference; + public final int bytesBetweenReference; + public final int millisecondsBetweenReference; + public final int[] bytesDeviations; + public final int[] millisecondsDeviations; + + public MlltFrame( + int mpegFramesBetweenReference, + int bytesBetweenReference, + int millisecondsBetweenReference, + int[] bytesDeviations, + int[] millisecondsDeviations) { + super(ID); + this.mpegFramesBetweenReference = mpegFramesBetweenReference; + this.bytesBetweenReference = bytesBetweenReference; + this.millisecondsBetweenReference = millisecondsBetweenReference; + this.bytesDeviations = bytesDeviations; + this.millisecondsDeviations = millisecondsDeviations; + } + + /* package */ + MlltFrame(Parcel in) { + super(ID); + this.mpegFramesBetweenReference = in.readInt(); + this.bytesBetweenReference = in.readInt(); + this.millisecondsBetweenReference = in.readInt(); + this.bytesDeviations = Util.castNonNull(in.createIntArray()); + this.millisecondsDeviations = Util.castNonNull(in.createIntArray()); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + MlltFrame other = (MlltFrame) obj; + return mpegFramesBetweenReference == other.mpegFramesBetweenReference + && bytesBetweenReference == other.bytesBetweenReference + && millisecondsBetweenReference == other.millisecondsBetweenReference + && Arrays.equals(bytesDeviations, other.bytesDeviations) + && Arrays.equals(millisecondsDeviations, other.millisecondsDeviations); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + mpegFramesBetweenReference; + result = 31 * result + bytesBetweenReference; + result = 31 * result + millisecondsBetweenReference; + result = 31 * result + Arrays.hashCode(bytesDeviations); + result = 31 * result + Arrays.hashCode(millisecondsDeviations); + return result; + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mpegFramesBetweenReference); + dest.writeInt(bytesBetweenReference); + dest.writeInt(millisecondsBetweenReference); + dest.writeIntArray(bytesDeviations); + dest.writeIntArray(millisecondsDeviations); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<MlltFrame> CREATOR = + new Creator<MlltFrame>() { + + @Override + public MlltFrame createFromParcel(Parcel in) { + return new MlltFrame(in); + } + + @Override + public MlltFrame[] newArray(int size) { + return new MlltFrame[size]; + } + }; +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/PrivFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/PrivFrame.java new file mode 100644 index 0000000000..248d9996dd --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/PrivFrame.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.Arrays; + +/** + * PRIV (Private) ID3 frame. + */ +public final class PrivFrame extends Id3Frame { + + public static final String ID = "PRIV"; + + public final String owner; + public final byte[] privateData; + + public PrivFrame(String owner, byte[] privateData) { + super(ID); + this.owner = owner; + this.privateData = privateData; + } + + /* package */ PrivFrame(Parcel in) { + super(ID); + owner = castNonNull(in.readString()); + privateData = castNonNull(in.createByteArray()); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + PrivFrame other = (PrivFrame) obj; + return Util.areEqual(owner, other.owner) && Arrays.equals(privateData, other.privateData); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (owner != null ? owner.hashCode() : 0); + result = 31 * result + Arrays.hashCode(privateData); + return result; + } + + @Override + public String toString() { + return id + ": owner=" + owner; + } + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(owner); + dest.writeByteArray(privateData); + } + + public static final Parcelable.Creator<PrivFrame> CREATOR = new Parcelable.Creator<PrivFrame>() { + + @Override + public PrivFrame createFromParcel(Parcel in) { + return new PrivFrame(in); + } + + @Override + public PrivFrame[] newArray(int size) { + return new PrivFrame[size]; + } + + }; + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java new file mode 100644 index 0000000000..c0bd36ccf7 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** + * Text information ID3 frame. + */ +public final class TextInformationFrame extends Id3Frame { + + @Nullable public final String description; + public final String value; + + public TextInformationFrame(String id, @Nullable String description, String value) { + super(id); + this.description = description; + this.value = value; + } + + /* package */ TextInformationFrame(Parcel in) { + super(castNonNull(in.readString())); + description = in.readString(); + value = castNonNull(in.readString()); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + TextInformationFrame other = (TextInformationFrame) obj; + return id.equals(other.id) && Util.areEqual(description, other.description) + && Util.areEqual(value, other.value); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + id.hashCode(); + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + (value != null ? value.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return id + ": description=" + description + ": value=" + value; + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(description); + dest.writeString(value); + } + + public static final Parcelable.Creator<TextInformationFrame> CREATOR = + new Parcelable.Creator<TextInformationFrame>() { + + @Override + public TextInformationFrame createFromParcel(Parcel in) { + return new TextInformationFrame(in); + } + + @Override + public TextInformationFrame[] newArray(int size) { + return new TextInformationFrame[size]; + } + + }; + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java new file mode 100644 index 0000000000..ced474960e --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java @@ -0,0 +1,96 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** + * Url link ID3 frame. + */ +public final class UrlLinkFrame extends Id3Frame { + + @Nullable public final String description; + public final String url; + + public UrlLinkFrame(String id, @Nullable String description, String url) { + super(id); + this.description = description; + this.url = url; + } + + /* package */ UrlLinkFrame(Parcel in) { + super(castNonNull(in.readString())); + description = in.readString(); + url = castNonNull(in.readString()); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + UrlLinkFrame other = (UrlLinkFrame) obj; + return id.equals(other.id) && Util.areEqual(description, other.description) + && Util.areEqual(url, other.url); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + id.hashCode(); + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + (url != null ? url.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return id + ": url=" + url; + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(description); + dest.writeString(url); + } + + public static final Parcelable.Creator<UrlLinkFrame> CREATOR = + new Parcelable.Creator<UrlLinkFrame>() { + + @Override + public UrlLinkFrame createFromParcel(Parcel in) { + return new UrlLinkFrame(in); + } + + @Override + public UrlLinkFrame[] newArray(int size) { + return new UrlLinkFrame[size]; + } + + }; + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/package-info.java new file mode 100644 index 0000000000..87b20161df --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; |