diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/AbstractContentMetadata.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/AbstractContentMetadata.java new file mode 100644 index 0000000000..c6df798c54 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/AbstractContentMetadata.java @@ -0,0 +1,151 @@ +/* + * 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 com.google.android.exoplayer2.upstream.cache; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; +import com.google.android.exoplayer2.util.Assertions; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** Abstract implementation of {@link ContentMetadata}. Values are stored as byte arrays. */ +public abstract class AbstractContentMetadata implements ContentMetadata { + + private final Map metadata; + + protected AbstractContentMetadata() { + this.metadata = new HashMap<>(); + } + + /** @param metadata Initial name value pairs. */ + protected AbstractContentMetadata(Map metadata) { + this.metadata = new HashMap<>(metadata); + } + + @Override + public final Editor edit() { + return new EditorImpl(); + } + + @Override + public final byte[] get(String name, byte[] defaultValue) { + synchronized (metadata) { + if (metadata.containsKey(name)) { + return metadata.get(name); + } else { + return defaultValue; + } + } + } + + @Override + public final String get(String name, String defaultValue) { + synchronized (metadata) { + if (metadata.containsKey(name)) { + byte[] bytes = metadata.get(name); + return new String(bytes, Charset.forName(C.UTF8_NAME)); + } else { + return defaultValue; + } + } + } + + @Override + public final long get(String name, long defaultValue) { + synchronized (metadata) { + if (metadata.containsKey(name)) { + byte[] bytes = metadata.get(name); + return ByteBuffer.wrap(bytes).getLong(); + } else { + return defaultValue; + } + } + } + + @Override + public final boolean contains(String name) { + synchronized (metadata) { + return metadata.containsKey(name); + } + } + + /** + * Called when any metadata value is changed or removed. {@code metadataValues} shouldn't be + * accessed out of this method. + * + * @param metadataValues All metadata name, value pairs. + */ + protected abstract void onChange(Map metadataValues) throws CacheException; + + private void apply(ArrayList removedValues, Map editedValues) + throws CacheException { + synchronized (metadata) { + for (int i = 0; i < removedValues.size(); i++) { + metadata.remove(removedValues.get(i)); + } + metadata.putAll(editedValues); + onChange(Collections.unmodifiableMap(metadata)); + } + } + + private class EditorImpl implements Editor { + + private final Map editedValues; + private final ArrayList removedValues; + + private EditorImpl() { + editedValues = new HashMap<>(); + removedValues = new ArrayList<>(); + } + + @Override + public Editor set(String name, String value) { + set(name, value.getBytes()); + return this; + } + + @Override + public Editor set(String name, long value) { + set(name, ByteBuffer.allocate(8).putLong(value).array()); + return this; + } + + @Override + public Editor set(String name, byte[] value) { + editedValues.put(name, Assertions.checkNotNull(value)); + removedValues.remove(name); + return this; + } + + @Override + public Editor remove(String name) { + removedValues.add(name); + editedValues.remove(name); + return this; + } + + @Override + public void commit() throws CacheException { + apply(removedValues, editedValues); + removedValues.clear(); + editedValues.clear(); + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java new file mode 100644 index 0000000000..7bff31853a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java @@ -0,0 +1,104 @@ +/* + * 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 com.google.android.exoplayer2.upstream.cache; + +import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; + +/** Interface for accessing cached content metadata which is stored as name, value pairs. */ +public interface ContentMetadata { + + /** + * Interface for modifying values in a {@link ContentMetadata} object. The changes you make in an + * editor are not copied back to the original {@link ContentMetadata} until you call {@link + * #commit()}. + */ + interface Editor { + /** + * Sets a metadata value, to be committed once {@link #commit()} is called. Passing {@code null} + * as {@code value} isn't allowed. {@code value} byte array shouldn't be modified after passed + * to this method. + * + * @param name The name of the metadata value. + * @param value The value to be set. + * @return This Editor instance, for convenience. + */ + Editor set(String name, byte[] value); + /** + * Sets a metadata value, to be committed once {@link #commit()} is called. Passing {@code null} + * as value isn't allowed. + * + * @param name The name of the metadata value. + * @param value The value to be set. + * @return This Editor instance, for convenience. + */ + Editor set(String name, String value); + /** + * Sets a metadata value, to be committed once {@link #commit()} is called. + * + * @param name The name of the metadata value. + * @param value The value to be set. + * @return This Editor instance, for convenience. + */ + Editor set(String name, long value); + /** + * Sets a metadata value, to be committed once {@link #commit()} is called. Passing {@code null} + * as value isn't allowed. + * + * @param name The name of the metadata value. + * @return This Editor instance, for convenience. + */ + Editor remove(String name); + /** + * Commits changes. It can be called only once. + * + * @throws CacheException If the commit fails. + */ + void commit() throws CacheException; + } + + /** Returns an editor to change metadata values. */ + Editor edit(); + + /** + * Returns a metadata value. + * + * @param name Name of the metadata to be returned. + * @param defaultValue Value to return if the metadata doesn't exist. + * @return The metadata value. + */ + byte[] get(String name, byte[] defaultValue); + + /** + * Returns a metadata value. + * + * @param name Name of the metadata to be returned. + * @param defaultValue Value to return if the metadata doesn't exist. + * @return The metadata value. + */ + String get(String name, String defaultValue); + + /** + * Returns a metadata value. + * + * @param name Name of the metadata to be returned. + * @param defaultValue Value to return if the metadata doesn't exist. + * @return The metadata value. + */ + long get(String name, long defaultValue); + + /** Returns whether the metadata is available. */ + boolean contains(String name); +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/AbstractContentMetadataTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/AbstractContentMetadataTest.java new file mode 100644 index 0000000000..5dc99bc654 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/AbstractContentMetadataTest.java @@ -0,0 +1,188 @@ +/* + * 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 com.google.android.exoplayer2.upstream.cache; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; +import com.google.android.exoplayer2.upstream.cache.ContentMetadata.Editor; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Tests {@link AbstractContentMetadata}. */ +@RunWith(RobolectricTestRunner.class) +public class AbstractContentMetadataTest { + + private FakeAbstractContentMetadata contentMetadata; + + @Before + public void setUp() throws Exception { + contentMetadata = createAbstractContentMetadata(); + } + + @Test + public void testContainsReturnsFalseWhenEmpty() throws Exception { + assertThat(contentMetadata.contains("test metadata")).isFalse(); + } + + @Test + public void testContainsReturnsTrueForInitialValue() throws Exception { + contentMetadata = createAbstractContentMetadata("metadata name", "value"); + assertThat(contentMetadata.contains("metadata name")).isTrue(); + } + + @Test + public void testGetReturnsDefaultValueWhenValueIsNotAvailable() throws Exception { + assertThat(contentMetadata.get("metadata name", "default value")).isEqualTo("default value"); + } + + @Test + public void testGetReturnsInitialValue() throws Exception { + contentMetadata = createAbstractContentMetadata("metadata name", "value"); + assertThat(contentMetadata.get("metadata name", "default value")).isEqualTo("value"); + } + + @Test + public void testEditReturnsAnEditor() throws Exception { + assertThat(contentMetadata.edit()).isNotNull(); + } + + @Test + public void testEditReturnsAnotherEditorEveryTime() throws Exception { + assertThat(contentMetadata.edit()).isNotEqualTo(contentMetadata.edit()); + } + + @Test + public void testCommitWithoutEditDoesNotFail() throws Exception { + Editor editor = contentMetadata.edit(); + editor.commit(); + } + + @Test + public void testAddNewMetadata() throws Exception { + Editor editor = contentMetadata.edit(); + editor.set("metadata name", "value"); + editor.commit(); + assertThat(contentMetadata.get("metadata name", "default value")).isEqualTo("value"); + } + + @Test + public void testAddNewIntMetadata() throws Exception { + Editor editor = contentMetadata.edit(); + editor.set("metadata name", 5); + editor.commit(); + assertThat(contentMetadata.get("metadata name", 0)).isEqualTo(5); + } + + @Test + public void testAddNewByteArrayMetadata() throws Exception { + Editor editor = contentMetadata.edit(); + byte[] value = {1, 2, 3}; + editor.set("metadata name", value); + editor.commit(); + assertThat(contentMetadata.get("metadata name", new byte[] {})).isEqualTo(value); + } + + @Test + public void testNewMetadataNotWrittenBeforeCommitted() throws Exception { + Editor editor = contentMetadata.edit(); + editor.set("metadata name", "value"); + assertThat(contentMetadata.get("metadata name", "default value")).isEqualTo("default value"); + } + + @Test + public void testEditMetadata() throws Exception { + contentMetadata = createAbstractContentMetadata("metadata name", "value"); + Editor editor = contentMetadata.edit(); + editor.set("metadata name", "edited value"); + editor.commit(); + assertThat(contentMetadata.get("metadata name", "default value")).isEqualTo("edited value"); + } + + @Test + public void testRemoveMetadata() throws Exception { + contentMetadata = createAbstractContentMetadata("metadata name", "value"); + Editor editor = contentMetadata.edit(); + editor.remove("metadata name"); + editor.commit(); + assertThat(contentMetadata.get("metadata name", "default value")).isEqualTo("default value"); + } + + @Test + public void testAddAndRemoveMetadata() throws Exception { + Editor editor = contentMetadata.edit(); + editor.set("metadata name", "value"); + editor.remove("metadata name"); + editor.commit(); + assertThat(contentMetadata.get("metadata name", "default value")).isEqualTo("default value"); + } + + @Test + public void testRemoveAndAddMetadata() throws Exception { + Editor editor = contentMetadata.edit(); + editor.remove("metadata name"); + editor.set("metadata name", "value"); + editor.commit(); + assertThat(contentMetadata.get("metadata name", "default value")).isEqualTo("value"); + } + + @Test + public void testOnChangeIsCalledWhenMetadataEdited() throws Exception { + contentMetadata = + createAbstractContentMetadata( + "metadata name", "value", "metadata name2", "value2", "metadata name3", "value3"); + Editor editor = contentMetadata.edit(); + editor.set("metadata name", "edited value"); + editor.remove("metadata name2"); + editor.commit(); + assertThat(contentMetadata.remainingValues).containsExactly("metadata name", "metadata name3"); + } + + private FakeAbstractContentMetadata createAbstractContentMetadata(String... pairs) { + assertThat(pairs.length % 2).isEqualTo(0); + HashMap map = new HashMap<>(); + for (int i = 0; i < pairs.length; i += 2) { + map.put(pairs[i], getBytes(pairs[i + 1])); + } + return new FakeAbstractContentMetadata(Collections.unmodifiableMap(map)); + } + + private static byte[] getBytes(String value) { + return value.getBytes(Charset.forName(C.UTF8_NAME)); + } + + private static class FakeAbstractContentMetadata extends AbstractContentMetadata { + + private ArrayList remainingValues; + + private FakeAbstractContentMetadata(Map metadataValues) { + super(metadataValues); + } + + @Override + protected void onChange(Map metadataValues) throws CacheException { + remainingValues = new ArrayList<>(metadataValues.keySet()); + } + } +}