Introduce SessionPlaylistAgent
Bug: 74090741
Test: SessionPlaylistAgentTest
Change-Id: I7fdff75e9f42e3d38f4bb08ca904706b25ecc884
diff --git a/packages/MediaComponents/Android.mk b/packages/MediaComponents/Android.mk
index b0d8e7d..b6480a2 100644
--- a/packages/MediaComponents/Android.mk
+++ b/packages/MediaComponents/Android.mk
@@ -40,6 +40,9 @@
LOCAL_PROGUARD_FLAG_FILES := proguard.cfg
+# TODO: Enable proguard (b/74090741)
+LOCAL_PROGUARD_ENABLED := disabled
+
LOCAL_MULTILIB := first
LOCAL_JAVA_LIBRARIES += android-support-annotations
diff --git a/packages/MediaComponents/src/com/android/media/SessionPlaylistAgent.java b/packages/MediaComponents/src/com/android/media/SessionPlaylistAgent.java
new file mode 100644
index 0000000..6805dad
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/media/SessionPlaylistAgent.java
@@ -0,0 +1,433 @@
+/*
+ * Copyright 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.android.media;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.media.DataSourceDesc;
+import android.media.MediaItem2;
+import android.media.MediaMetadata2;
+import android.media.MediaPlayerBase;
+import android.media.MediaPlaylistAgent;
+import android.media.MediaSession2;
+import android.media.MediaSession2.OnDataSourceMissingHelper;
+import android.util.ArrayMap;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ThreadLocalRandom;
+
+public class SessionPlaylistAgent extends MediaPlaylistAgent {
+ private static final String TAG = "SessionPlaylistAgent";
+ @VisibleForTesting
+ static final int END_OF_PLAYLIST = -1;
+ @VisibleForTesting
+ static final int NO_VALID_ITEMS = -2;
+
+ private final PlayItem mEopPlayItem = new PlayItem(END_OF_PLAYLIST, null);
+
+ private final Object mLock = new Object();
+ private final MediaSession2 mSession;
+
+ // TODO: Set data sources properly into mPlayer (b/74090741)
+ @GuardedBy("mLock")
+ private MediaPlayerBase mPlayer;
+ @GuardedBy("mLock")
+ private OnDataSourceMissingHelper mDsdHelper;
+
+ // TODO: Check if having the same item is okay (b/74090741)
+ @GuardedBy("mLock")
+ private ArrayList<MediaItem2> mPlaylist = new ArrayList<>();
+ @GuardedBy("mLock")
+ private ArrayList<MediaItem2> mShuffledList = new ArrayList<>();
+ @GuardedBy("mLock")
+ private Map<MediaItem2, DataSourceDesc> mItemDsdMap = new ArrayMap<>();
+ @GuardedBy("mLock")
+ private MediaMetadata2 mMetadata;
+ @GuardedBy("mLock")
+ private int mRepeatMode;
+ @GuardedBy("mLock")
+ private int mShuffleMode;
+ @GuardedBy("mLock")
+ private PlayItem mCurrent;
+
+ private class PlayItem {
+ int shuffledIdx;
+ DataSourceDesc dsd;
+ MediaItem2 mediaItem;
+
+ PlayItem(int shuffledIdx) {
+ this(shuffledIdx, null);
+ }
+
+ PlayItem(int shuffledIdx, DataSourceDesc dsd) {
+ this.shuffledIdx = shuffledIdx;
+ if (shuffledIdx >= 0) {
+ this.mediaItem = mShuffledList.get(shuffledIdx);
+ if (dsd == null) {
+ synchronized (mLock) {
+ this.dsd = retrieveDataSourceDescLocked(this.mediaItem);
+ }
+ } else {
+ this.dsd = dsd;
+ }
+ }
+ }
+
+ boolean isValid() {
+ if (this == mEopPlayItem) {
+ return true;
+ }
+ if (mediaItem == null) {
+ return false;
+ }
+ if (dsd == null) {
+ return false;
+ }
+ if (shuffledIdx >= mShuffledList.size()) {
+ return false;
+ }
+ if (mediaItem != mShuffledList.get(shuffledIdx)) {
+ return false;
+ }
+ if (mediaItem.getDataSourceDesc() != null
+ && !mediaItem.getDataSourceDesc().equals(dsd)) {
+ return false;
+ }
+ return true;
+ }
+ }
+
+ public SessionPlaylistAgent(@NonNull Context context, @NonNull MediaSession2 session,
+ @NonNull MediaPlayerBase player) {
+ super(context);
+ if (session == null) {
+ throw new IllegalArgumentException("session shouldn't be null");
+ }
+ if (player == null) {
+ throw new IllegalArgumentException("player shouldn't be null");
+ }
+ mSession = session;
+ mPlayer = player;
+ }
+
+ public void setPlayer(MediaPlayerBase player) {
+ if (player == null) {
+ throw new IllegalArgumentException("player shouldn't be null");
+ }
+ synchronized (mLock) {
+ mPlayer = player;
+ }
+ }
+
+ public void setOnDataSourceMissingHelper(OnDataSourceMissingHelper helper) {
+ synchronized (mLock) {
+ mDsdHelper = helper;
+ }
+ }
+
+ @Override
+ public @Nullable List<MediaItem2> getPlaylist() {
+ synchronized (mLock) {
+ return Collections.unmodifiableList(mPlaylist);
+ }
+ }
+
+ @Override
+ public void setPlaylist(@NonNull List<MediaItem2> list,
+ @Nullable MediaMetadata2 metadata) {
+ if (list == null) {
+ throw new IllegalArgumentException("list shouldn't be null");
+ }
+
+ synchronized (mLock) {
+ mItemDsdMap.clear();
+
+ mPlaylist.clear();
+ mPlaylist.addAll(list);
+ applyShuffleModeLocked();
+
+ mMetadata = metadata;
+ mCurrent = getNextValidPlayItemLocked(END_OF_PLAYLIST, 1);
+ }
+ notifyPlaylistChanged();
+ }
+
+ @Override
+ public @Nullable MediaMetadata2 getPlaylistMetadata() {
+ return mMetadata;
+ }
+
+ @Override
+ public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
+ synchronized (mLock) {
+ if (metadata == mMetadata) {
+ return;
+ }
+ mMetadata = metadata;
+ }
+ notifyPlaylistMetadataChanged();
+ }
+
+ @Override
+ public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
+ if (item == null) {
+ throw new IllegalArgumentException("item shouldn't be null");
+ }
+ synchronized (mLock) {
+ index = clamp(index, mPlaylist.size());
+ int shuffledIdx = index;
+ mPlaylist.add(index, item);
+ if (mShuffleMode == MediaPlaylistAgent.SHUFFLE_MODE_NONE) {
+ mShuffledList.add(index, item);
+ } else {
+ // Add the item in random position of mShuffledList.
+ shuffledIdx = ThreadLocalRandom.current().nextInt(mShuffledList.size() + 1);
+ mShuffledList.add(shuffledIdx, item);
+ }
+ if (!hasValidItem()) {
+ mCurrent = getNextValidPlayItemLocked(END_OF_PLAYLIST, 1);
+ } else {
+ updateCurrentIfNeededLocked();
+ }
+ }
+ notifyPlaylistChanged();
+ }
+
+ @Override
+ public void removePlaylistItem(@NonNull MediaItem2 item) {
+ if (item == null) {
+ throw new IllegalArgumentException("item shouldn't be null");
+ }
+ synchronized (mLock) {
+ if (!mPlaylist.remove(item)) {
+ return;
+ }
+ mShuffledList.remove(item);
+ mItemDsdMap.remove(item);
+ updateCurrentIfNeededLocked();
+ }
+ notifyPlaylistChanged();
+ }
+
+ @Override
+ public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
+ if (item == null) {
+ throw new IllegalArgumentException("item shouldn't be null");
+ }
+ synchronized (mLock) {
+ if (mPlaylist.size() <= 0) {
+ return;
+ }
+ index = clamp(index, mPlaylist.size() - 1);
+ int shuffledIdx = mShuffledList.indexOf(mPlaylist.get(index));
+ mItemDsdMap.remove(mShuffledList.get(shuffledIdx));
+ mShuffledList.set(shuffledIdx, item);
+ mPlaylist.set(index, item);
+ if (!hasValidItem()) {
+ mCurrent = getNextValidPlayItemLocked(END_OF_PLAYLIST, 1);
+ } else {
+ updateCurrentIfNeededLocked();
+ }
+ }
+ notifyPlaylistChanged();
+ }
+
+ @Override
+ public void skipToPlaylistItem(@NonNull MediaItem2 item) {
+ if (item == null) {
+ throw new IllegalArgumentException("item shouldn't be null");
+ }
+ synchronized (mLock) {
+ if (!hasValidItem() || item.equals(mCurrent.mediaItem)) {
+ return;
+ }
+ int shuffledIdx = mShuffledList.indexOf(item);
+ if (shuffledIdx < 0) {
+ return;
+ }
+ mCurrent = new PlayItem(shuffledIdx);
+ updateCurrentIfNeededLocked();
+ }
+ }
+
+ @Override
+ public void skipToPreviousItem() {
+ synchronized (mLock) {
+ if (!hasValidItem()) {
+ return;
+ }
+ PlayItem prev = getNextValidPlayItemLocked(mCurrent.shuffledIdx, -1);
+ if (prev != mEopPlayItem) {
+ mCurrent = prev;
+ }
+ updateCurrentIfNeededLocked();
+ }
+ }
+
+ @Override
+ public void skipToNextItem() {
+ synchronized (mLock) {
+ if (!hasValidItem()) {
+ return;
+ }
+ PlayItem next = getNextValidPlayItemLocked(mCurrent.shuffledIdx, 1);
+ if (next != mEopPlayItem) {
+ mCurrent = next;
+ }
+ updateCurrentIfNeededLocked();
+ }
+ }
+
+ @Override
+ public int getRepeatMode() {
+ return mRepeatMode;
+ }
+
+ @Override
+ public void setRepeatMode(int repeatMode) {
+ if (repeatMode < MediaPlaylistAgent.REPEAT_MODE_NONE
+ || repeatMode > MediaPlaylistAgent.REPEAT_MODE_GROUP) {
+ return;
+ }
+ synchronized (mLock) {
+ if (mRepeatMode == repeatMode) {
+ return;
+ }
+ mRepeatMode = repeatMode;
+ }
+ notifyRepeatModeChanged();
+ }
+
+ @Override
+ public int getShuffleMode() {
+ return mShuffleMode;
+ }
+
+ @Override
+ public void setShuffleMode(int shuffleMode) {
+ if (shuffleMode < MediaPlaylistAgent.SHUFFLE_MODE_NONE
+ || shuffleMode > MediaPlaylistAgent.SHUFFLE_MODE_GROUP) {
+ return;
+ }
+ synchronized (mLock) {
+ if (mShuffleMode == shuffleMode) {
+ return;
+ }
+ mShuffleMode = shuffleMode;
+ applyShuffleModeLocked();
+ }
+ notifyShuffleModeChanged();
+ }
+
+ @VisibleForTesting
+ int getCurShuffledIndex() {
+ return hasValidItem() ? mCurrent.shuffledIdx : NO_VALID_ITEMS;
+ }
+
+ private boolean hasValidItem() {
+ return mCurrent != null;
+ }
+
+ private DataSourceDesc retrieveDataSourceDescLocked(MediaItem2 item) {
+ DataSourceDesc dsd = item.getDataSourceDesc();
+ if (dsd != null) {
+ mItemDsdMap.put(item, dsd);
+ return dsd;
+ }
+ dsd = mItemDsdMap.get(item);
+ if (dsd != null) {
+ return dsd;
+ }
+ OnDataSourceMissingHelper helper = mDsdHelper;
+ if (helper != null) {
+ // TODO: Do not call onDataSourceMissing with the lock (b/74090741).
+ dsd = helper.onDataSourceMissing(mSession, item);
+ if (dsd != null) {
+ mItemDsdMap.put(item, dsd);
+ }
+ }
+ return dsd;
+ }
+
+ private PlayItem getNextValidPlayItemLocked(int curShuffledIdx, int direction) {
+ int size = mPlaylist.size();
+ if (curShuffledIdx == END_OF_PLAYLIST) {
+ curShuffledIdx = (direction > 0) ? -1 : size;
+ }
+ for (int i = 0; i < size; i++) {
+ curShuffledIdx += direction;
+ if (curShuffledIdx < 0 || curShuffledIdx >= mPlaylist.size()) {
+ if (mRepeatMode == REPEAT_MODE_NONE) {
+ return (i == size - 1) ? null : mEopPlayItem;
+ } else {
+ curShuffledIdx = curShuffledIdx < 0 ? mPlaylist.size() - 1 : 0;
+ }
+ }
+ DataSourceDesc dsd = retrieveDataSourceDescLocked(mShuffledList.get(curShuffledIdx));
+ if (dsd != null) {
+ return new PlayItem(curShuffledIdx, dsd);
+ }
+ }
+ return null;
+ }
+
+ private void updateCurrentIfNeededLocked() {
+ if (!hasValidItem() || mCurrent.isValid()) {
+ return;
+ }
+ int shuffledIdx = mShuffledList.indexOf(mCurrent.mediaItem);
+ if (shuffledIdx >= 0) {
+ // Added an item.
+ mCurrent.shuffledIdx = shuffledIdx;
+ return;
+ }
+
+ if (mCurrent.shuffledIdx >= mShuffledList.size()) {
+ mCurrent = getNextValidPlayItemLocked(mShuffledList.size() - 1, 1);
+ } else {
+ mCurrent.mediaItem = mShuffledList.get(mCurrent.shuffledIdx);
+ if (retrieveDataSourceDescLocked(mCurrent.mediaItem) == null) {
+ mCurrent = getNextValidPlayItemLocked(mCurrent.shuffledIdx, 1);
+ }
+ }
+ return;
+ }
+
+ private void applyShuffleModeLocked() {
+ mShuffledList.clear();
+ mShuffledList.addAll(mPlaylist);
+ if (mShuffleMode == MediaPlaylistAgent.SHUFFLE_MODE_ALL
+ || mShuffleMode == MediaPlaylistAgent.SHUFFLE_MODE_GROUP) {
+ Collections.shuffle(mShuffledList);
+ }
+ }
+
+ // Clamps value to [0, size]
+ private static int clamp(int value, int size) {
+ if (value < 0) {
+ return 0;
+ }
+ return (value > size) ? size : value;
+ }
+}
diff --git a/packages/MediaComponents/tests/Android.mk b/packages/MediaComponents/tests/Android.mk
new file mode 100644
index 0000000..bf81efb
--- /dev/null
+++ b/packages/MediaComponents/tests/Android.mk
@@ -0,0 +1,35 @@
+# Copyright 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.
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ android.test.runner.stubs \
+ android.test.base.stubs \
+ junit
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := MediaComponentsTest
+
+LOCAL_INSTRUMENTATION_FOR := MediaComponents
+
+LOCAL_PRIVATE_PLATFORM_APIS := true
+
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
diff --git a/packages/MediaComponents/tests/AndroidManifest.xml b/packages/MediaComponents/tests/AndroidManifest.xml
new file mode 100644
index 0000000..7255265
--- /dev/null
+++ b/packages/MediaComponents/tests/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.media.tests">
+
+ <application android:label="Media API Test">
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <!--
+ To run the tests use the command:
+ "adb shell am instrument -w com.android.media.tests/android.test.InstrumentationTestRunner"
+ -->
+ <instrumentation
+ android:name="android.test.InstrumentationTestRunner"
+ android:targetPackage="com.android.media.update"
+ android:label="Media API test" />
+
+</manifest>
diff --git a/packages/MediaComponents/tests/src/com/android/media/SessionPlaylistAgentTest.java b/packages/MediaComponents/tests/src/com/android/media/SessionPlaylistAgentTest.java
new file mode 100644
index 0000000..80370ec
--- /dev/null
+++ b/packages/MediaComponents/tests/src/com/android/media/SessionPlaylistAgentTest.java
@@ -0,0 +1,515 @@
+/*
+ * Copyright 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.android.media;
+
+import android.content.Context;
+import android.media.DataSourceDesc;
+import android.media.MediaItem2;
+import android.media.MediaMetadata2;
+import android.media.MediaPlayer2;
+import android.media.MediaPlayerBase;
+import android.media.MediaPlaylistAgent;
+import android.media.MediaSession2;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Tests {@link SessionPlaylistAgent}.
+ */
+public class SessionPlaylistAgentTest extends AndroidTestCase {
+ private static final String TAG = "SessionPlaylistAgentTest";
+ private static final int WAIT_TIME_MS = 1000;
+ private static final int INVALID_REPEAT_MODE = -100;
+ private static final int INVALID_SHUFFLE_MODE = -100;
+
+ private Handler mHandler;
+ private Executor mHandlerExecutor;
+
+ private Object mWaitLock = new Object();
+ private Context mContext;
+ private MediaSession2 mSession;
+ private MediaPlayerBase mPlayer;
+ private SessionPlaylistAgent mAgent;
+ private MyDataSourceHelper mDataSourceHelper;
+ private MyPlaylistEventCallback mEventCallback;
+
+ public class MyPlaylistEventCallback extends MediaPlaylistAgent.PlaylistEventCallback {
+ boolean onPlaylistChangedCalled;
+ boolean onPlaylistMetadataChangedCalled;
+ boolean onRepeatModeChangedCalled;
+ boolean onShuffleModeChangedCalled;
+
+ private Object mWaitLock;
+
+ public MyPlaylistEventCallback(Object waitLock) {
+ mWaitLock = waitLock;
+ }
+
+ public void clear() {
+ onPlaylistChangedCalled = false;
+ onPlaylistMetadataChangedCalled = false;
+ onRepeatModeChangedCalled = false;
+ onShuffleModeChangedCalled = false;
+ }
+
+ public void onPlaylistChanged(MediaPlaylistAgent playlistAgent, List<MediaItem2> list,
+ MediaMetadata2 metadata) {
+ synchronized (mWaitLock) {
+ onPlaylistChangedCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ public void onPlaylistMetadataChanged(MediaPlaylistAgent playlistAgent,
+ MediaMetadata2 metadata) {
+ synchronized (mWaitLock) {
+ onPlaylistMetadataChangedCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ public void onRepeatModeChanged(MediaPlaylistAgent playlistAgent, int repeatMode) {
+ synchronized (mWaitLock) {
+ onRepeatModeChangedCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ public void onShuffleModeChanged(MediaPlaylistAgent playlistAgent, int shuffleMode) {
+ synchronized (mWaitLock) {
+ onShuffleModeChangedCalled = true;
+ mWaitLock.notify();
+ }
+ }
+ }
+
+ public class MyDataSourceHelper implements MediaSession2.OnDataSourceMissingHelper {
+ @Override
+ public DataSourceDesc onDataSourceMissing(MediaSession2 session, MediaItem2 item) {
+ if (item.getMediaId().contains("WITHOUT_DSD")) {
+ return null;
+ }
+ return new DataSourceDesc.Builder()
+ .setDataSource(getContext(), Uri.parse("dsd://test"))
+ .setMediaId(item.getMediaId())
+ .build();
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ HandlerThread handlerThread = new HandlerThread("SessionPlaylistAgent");
+ handlerThread.start();
+ mHandler = new Handler(handlerThread.getLooper());
+ mHandlerExecutor = (runnable) -> {
+ mHandler.post(runnable);
+ };
+
+ mContext = getContext();
+ mPlayer = MediaPlayer2.create();
+ mSession = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(mHandlerExecutor,
+ new MediaSession2.SessionCallback(mContext) {})
+ .setId(TAG).build();
+ mDataSourceHelper = new MyDataSourceHelper();
+ mAgent = new SessionPlaylistAgent(mContext, mSession, mPlayer);
+ mAgent.setOnDataSourceMissingHelper(mDataSourceHelper);
+ mEventCallback = new MyPlaylistEventCallback(mWaitLock);
+ mAgent.registerPlaylistEventCallback(mHandlerExecutor, mEventCallback);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mSession.close();
+ mPlayer.close();
+ mHandler.getLooper().quitSafely();
+ mHandler = null;
+ mHandlerExecutor = null;
+ }
+
+ @Test
+ public void testSetAndGetShuflleMode() throws Exception {
+ int shuffleMode = mAgent.getShuffleMode();
+ if (shuffleMode != MediaPlaylistAgent.SHUFFLE_MODE_NONE) {
+ mEventCallback.clear();
+ synchronized (mWaitLock) {
+ mAgent.setShuffleMode(MediaPlaylistAgent.SHUFFLE_MODE_NONE);
+ mWaitLock.wait(WAIT_TIME_MS);
+ assertTrue(mEventCallback.onShuffleModeChangedCalled);
+ }
+ assertEquals(MediaPlaylistAgent.SHUFFLE_MODE_NONE, mAgent.getShuffleMode());
+ }
+
+ mEventCallback.clear();
+ synchronized (mWaitLock) {
+ mAgent.setShuffleMode(MediaPlaylistAgent.SHUFFLE_MODE_ALL);
+ mWaitLock.wait(WAIT_TIME_MS);
+ assertTrue(mEventCallback.onShuffleModeChangedCalled);
+ }
+ assertEquals(MediaPlaylistAgent.SHUFFLE_MODE_ALL, mAgent.getShuffleMode());
+
+ mEventCallback.clear();
+ synchronized (mWaitLock) {
+ mAgent.setShuffleMode(MediaPlaylistAgent.SHUFFLE_MODE_GROUP);
+ mWaitLock.wait(WAIT_TIME_MS);
+ assertTrue(mEventCallback.onShuffleModeChangedCalled);
+ }
+ assertEquals(MediaPlaylistAgent.SHUFFLE_MODE_GROUP, mAgent.getShuffleMode());
+
+ // INVALID_SHUFFLE_MODE will not change the shuffle mode.
+ mEventCallback.clear();
+ synchronized (mWaitLock) {
+ mAgent.setShuffleMode(INVALID_SHUFFLE_MODE);
+ mWaitLock.wait(WAIT_TIME_MS);
+ assertFalse(mEventCallback.onShuffleModeChangedCalled);
+ }
+ assertEquals(MediaPlaylistAgent.SHUFFLE_MODE_GROUP, mAgent.getShuffleMode());
+ }
+
+ @Test
+ public void testSetAndGetRepeatMode() throws Exception {
+ int repeatMode = mAgent.getRepeatMode();
+ if (repeatMode != MediaPlaylistAgent.REPEAT_MODE_NONE) {
+ mEventCallback.clear();
+ synchronized (mWaitLock) {
+ mAgent.setRepeatMode(MediaPlaylistAgent.REPEAT_MODE_NONE);
+ mWaitLock.wait(WAIT_TIME_MS);
+ assertTrue(mEventCallback.onRepeatModeChangedCalled);
+ }
+ assertEquals(MediaPlaylistAgent.REPEAT_MODE_NONE, mAgent.getRepeatMode());
+ }
+
+ mEventCallback.clear();
+ synchronized (mWaitLock) {
+ mAgent.setRepeatMode(MediaPlaylistAgent.REPEAT_MODE_ONE);
+ mWaitLock.wait(WAIT_TIME_MS);
+ assertTrue(mEventCallback.onRepeatModeChangedCalled);
+ }
+ assertEquals(MediaPlaylistAgent.REPEAT_MODE_ONE, mAgent.getRepeatMode());
+
+ mEventCallback.clear();
+ synchronized (mWaitLock) {
+ mAgent.setRepeatMode(MediaPlaylistAgent.REPEAT_MODE_ALL);
+ mWaitLock.wait(WAIT_TIME_MS);
+ assertTrue(mEventCallback.onRepeatModeChangedCalled);
+ }
+ assertEquals(MediaPlaylistAgent.REPEAT_MODE_ALL, mAgent.getRepeatMode());
+
+ mEventCallback.clear();
+ synchronized (mWaitLock) {
+ mAgent.setRepeatMode(MediaPlaylistAgent.REPEAT_MODE_GROUP);
+ mWaitLock.wait(WAIT_TIME_MS);
+ assertTrue(mEventCallback.onRepeatModeChangedCalled);
+ }
+ assertEquals(MediaPlaylistAgent.REPEAT_MODE_GROUP, mAgent.getRepeatMode());
+
+ // INVALID_SHUFFLE_MODE will not change the shuffle mode.
+ mEventCallback.clear();
+ synchronized (mWaitLock) {
+ mAgent.setRepeatMode(INVALID_REPEAT_MODE);
+ mWaitLock.wait(WAIT_TIME_MS);
+ assertFalse(mEventCallback.onRepeatModeChangedCalled);
+ }
+ assertEquals(MediaPlaylistAgent.REPEAT_MODE_GROUP, mAgent.getRepeatMode());
+ }
+
+ @Test
+ public void testSetPlaylist() throws Exception {
+ int listSize = 10;
+ createAndSetPlaylist(10);
+ assertEquals(listSize, mAgent.getPlaylist().size());
+ assertEquals(0, mAgent.getCurShuffledIndex());
+ }
+
+ @Test
+ public void testSkipItems() throws Exception {
+ int listSize = 5;
+ List<MediaItem2> playlist = createAndSetPlaylist(listSize);
+
+ mAgent.setRepeatMode(MediaPlaylistAgent.REPEAT_MODE_NONE);
+ // Test skipToPlaylistItem
+ for (int i = listSize - 1; i >= 0; --i) {
+ mAgent.skipToPlaylistItem(playlist.get(i));
+ assertEquals(i, mAgent.getCurShuffledIndex());
+ }
+
+ // Test skipToNextItem
+ // curPlayPos = 0
+ for (int curPlayPos = 0; curPlayPos < listSize - 1; ++curPlayPos) {
+ mAgent.skipToNextItem();
+ assertEquals(curPlayPos + 1, mAgent.getCurShuffledIndex());
+ }
+ mAgent.skipToNextItem();
+ assertEquals(listSize - 1, mAgent.getCurShuffledIndex());
+
+ // Test skipToPrevious
+ // curPlayPos = listSize - 1
+ for (int curPlayPos = listSize - 1; curPlayPos > 0; --curPlayPos) {
+ mAgent.skipToPreviousItem();
+ assertEquals(curPlayPos - 1, mAgent.getCurShuffledIndex());
+ }
+ mAgent.skipToPreviousItem();
+ assertEquals(0, mAgent.getCurShuffledIndex());
+
+ mAgent.setRepeatMode(MediaPlaylistAgent.REPEAT_MODE_ALL);
+ // Test skipToPrevious with repeat mode all
+ // curPlayPos = 0
+ mAgent.skipToPreviousItem();
+ assertEquals(listSize - 1, mAgent.getCurShuffledIndex());
+
+ // Test skipToNext with repeat mode all
+ // curPlayPos = listSize - 1
+ mAgent.skipToNextItem();
+ assertEquals(0, mAgent.getCurShuffledIndex());
+
+ mAgent.skipToPreviousItem();
+ // curPlayPos = listSize - 1, nextPlayPos = 0
+ // Test next play pos after setting repeat mode none.
+ mAgent.setRepeatMode(MediaPlaylistAgent.REPEAT_MODE_NONE);
+ assertEquals(listSize - 1, mAgent.getCurShuffledIndex());
+ }
+
+ @Test
+ public void testEditPlaylist() throws Exception {
+ int listSize = 5;
+ List<MediaItem2> playlist = createAndSetPlaylist(listSize);
+
+ // Test add item: [0 (cur), 1, 2, 3, 4] -> [0 (cur), 1, 5, 2, 3, 4]
+ mEventCallback.clear();
+ MediaItem2 item_5 = generateMediaItem(5);
+ synchronized (mWaitLock) {
+ playlist.add(2, item_5);
+ mAgent.addPlaylistItem(2, item_5);
+ mWaitLock.wait(WAIT_TIME_MS);
+ assertTrue(mEventCallback.onPlaylistChangedCalled);
+ }
+ assertPlaylistEquals(playlist, mAgent.getPlaylist());
+
+ mEventCallback.clear();
+ // Move current: [0 (cur), 1, 5, 2, 3, 4] -> [0, 1, 5 (cur), 2, 3, 4]
+ mAgent.skipToPlaylistItem(item_5);
+ // Remove current item: [0, 1, 5 (cur), 2, 3, 4] -> [0, 1, 2 (cur), 3, 4]
+ synchronized (mWaitLock) {
+ playlist.remove(item_5);
+ mAgent.removePlaylistItem(item_5);
+ mWaitLock.wait(WAIT_TIME_MS);
+ assertTrue(mEventCallback.onPlaylistChangedCalled);
+ }
+ assertPlaylistEquals(playlist, mAgent.getPlaylist());
+ assertEquals(2, mAgent.getCurShuffledIndex());
+
+ // Remove previous item: [0, 1, 2 (cur), 3, 4] -> [0, 2 (cur), 3, 4]
+ mEventCallback.clear();
+ MediaItem2 previousItem = playlist.get(1);
+ synchronized (mWaitLock) {
+ playlist.remove(previousItem);
+ mAgent.removePlaylistItem(previousItem);
+ mWaitLock.wait(WAIT_TIME_MS);
+ assertTrue(mEventCallback.onPlaylistChangedCalled);
+ }
+ assertPlaylistEquals(playlist, mAgent.getPlaylist());
+ assertEquals(1, mAgent.getCurShuffledIndex());
+
+ // Remove next item: [0, 2 (cur), 3, 4] -> [0, 2 (cur), 4]
+ mEventCallback.clear();
+ MediaItem2 nextItem = playlist.get(2);
+ synchronized (mWaitLock) {
+ playlist.remove(nextItem);
+ mAgent.removePlaylistItem(nextItem);
+ mWaitLock.wait(WAIT_TIME_MS);
+ assertTrue(mEventCallback.onPlaylistChangedCalled);
+ }
+ assertPlaylistEquals(playlist, mAgent.getPlaylist());
+ assertEquals(1, mAgent.getCurShuffledIndex());
+
+ // Replace item: [0, 2 (cur), 4] -> [0, 2 (cur), 5]
+ mEventCallback.clear();
+ synchronized (mWaitLock) {
+ playlist.set(2, item_5);
+ mAgent.replacePlaylistItem(2, item_5);
+ mWaitLock.wait(WAIT_TIME_MS);
+ assertTrue(mEventCallback.onPlaylistChangedCalled);
+ }
+ assertPlaylistEquals(playlist, mAgent.getPlaylist());
+ assertEquals(1, mAgent.getCurShuffledIndex());
+
+ // Move last and remove the last item: [0, 2 (cur), 5] -> [0, 2, 5 (cur)] -> [0, 2 (cur)]
+ MediaItem2 lastItem = playlist.get(1);
+ mAgent.skipToPlaylistItem(lastItem);
+ mEventCallback.clear();
+ synchronized (mWaitLock) {
+ playlist.remove(lastItem);
+ mAgent.removePlaylistItem(lastItem);
+ mWaitLock.wait(WAIT_TIME_MS);
+ assertTrue(mEventCallback.onPlaylistChangedCalled);
+ }
+ assertPlaylistEquals(playlist, mAgent.getPlaylist());
+ assertEquals(1, mAgent.getCurShuffledIndex());
+
+ // Remove all items
+ for (int i = playlist.size() - 1; i >= 0; --i) {
+ MediaItem2 item = playlist.get(i);
+ mAgent.skipToPlaylistItem(item);
+ mEventCallback.clear();
+ synchronized (mWaitLock) {
+ playlist.remove(item);
+ mAgent.removePlaylistItem(item);
+ mWaitLock.wait(WAIT_TIME_MS);
+ assertTrue(mEventCallback.onPlaylistChangedCalled);
+ }
+ assertPlaylistEquals(playlist, mAgent.getPlaylist());
+ }
+ assertEquals(SessionPlaylistAgent.NO_VALID_ITEMS, mAgent.getCurShuffledIndex());
+ }
+
+
+ @Test
+ public void testPlaylistWithInvalidItem() throws Exception {
+ int listSize = 2;
+ List<MediaItem2> playlist = createAndSetPlaylist(listSize);
+
+ // Add item: [0 (cur), 1] -> [0 (cur), 3 (no_dsd), 1]
+ mEventCallback.clear();
+ MediaItem2 invalidItem2 = generateMediaItemWithoutDataSourceDesc(2);
+ synchronized (mWaitLock) {
+ playlist.add(1, invalidItem2);
+ mAgent.addPlaylistItem(1, invalidItem2);
+ mWaitLock.wait(WAIT_TIME_MS);
+ assertTrue(mEventCallback.onPlaylistChangedCalled);
+ }
+ assertPlaylistEquals(playlist, mAgent.getPlaylist());
+ assertEquals(0, mAgent.getCurShuffledIndex());
+
+ // Test skip to next item: [0 (cur), 2 (no_dsd), 1] -> [0, 2 (no_dsd), 1 (cur)]
+ mAgent.skipToNextItem();
+ assertEquals(2, mAgent.getCurShuffledIndex());
+
+ // Test skip to previous item: [0, 2 (no_dsd), 1 (cur)] -> [0 (cur), 2 (no_dsd), 1]
+ mAgent.skipToPreviousItem();
+ assertEquals(0, mAgent.getCurShuffledIndex());
+
+ // Remove current item: [0 (cur), 2 (no_dsd), 1] -> [2 (no_dsd), 1 (cur)]
+ mEventCallback.clear();
+ MediaItem2 item = playlist.get(0);
+ synchronized (mWaitLock) {
+ playlist.remove(item);
+ mAgent.removePlaylistItem(item);
+ mWaitLock.wait(WAIT_TIME_MS);
+ assertTrue(mEventCallback.onPlaylistChangedCalled);
+ }
+ assertPlaylistEquals(playlist, mAgent.getPlaylist());
+ assertEquals(1, mAgent.getCurShuffledIndex());
+
+ // Remove current item: [2 (no_dsd), 1 (cur)] -> [2 (no_dsd)]
+ mEventCallback.clear();
+ item = playlist.get(1);
+ synchronized (mWaitLock) {
+ playlist.remove(item);
+ mAgent.removePlaylistItem(item);
+ mWaitLock.wait(WAIT_TIME_MS);
+ assertTrue(mEventCallback.onPlaylistChangedCalled);
+ }
+ assertPlaylistEquals(playlist, mAgent.getPlaylist());
+ assertEquals(SessionPlaylistAgent.NO_VALID_ITEMS, mAgent.getCurShuffledIndex());
+
+ // Add invalid item: [2 (no_dsd)] -> [0 (no_dsd), 2 (no_dsd)]
+ MediaItem2 invalidItem0 = generateMediaItemWithoutDataSourceDesc(0);
+ mEventCallback.clear();
+ synchronized (mWaitLock) {
+ playlist.add(0, invalidItem0);
+ mAgent.addPlaylistItem(0, invalidItem0);
+ mWaitLock.wait(WAIT_TIME_MS);
+ assertTrue(mEventCallback.onPlaylistChangedCalled);
+ }
+ assertPlaylistEquals(playlist, mAgent.getPlaylist());
+ assertEquals(SessionPlaylistAgent.NO_VALID_ITEMS, mAgent.getCurShuffledIndex());
+
+ // Add valid item: [0 (no_dsd), 2 (no_dsd)] -> [0 (no_dsd), 1, 2 (no_dsd)]
+ MediaItem2 invalidItem1 = generateMediaItem(1);
+ mEventCallback.clear();
+ synchronized (mWaitLock) {
+ playlist.add(1, invalidItem1);
+ mAgent.addPlaylistItem(1, invalidItem1);
+ mWaitLock.wait(WAIT_TIME_MS);
+ assertTrue(mEventCallback.onPlaylistChangedCalled);
+ }
+ assertPlaylistEquals(playlist, mAgent.getPlaylist());
+ assertEquals(1, mAgent.getCurShuffledIndex());
+
+ // Replace the valid item with an invalid item:
+ // [0 (no_dsd), 1 (cur), 2 (no_dsd)] -> [0 (no_dsd), 3 (no_dsd), 2 (no_dsd)]
+ MediaItem2 invalidItem3 = generateMediaItemWithoutDataSourceDesc(3);
+ mEventCallback.clear();
+ synchronized (mWaitLock) {
+ playlist.set(1, invalidItem3);
+ mAgent.replacePlaylistItem(1, invalidItem3);
+ mWaitLock.wait(WAIT_TIME_MS);
+ assertTrue(mEventCallback.onPlaylistChangedCalled);
+ }
+ assertPlaylistEquals(playlist, mAgent.getPlaylist());
+ assertEquals(SessionPlaylistAgent.END_OF_PLAYLIST, mAgent.getCurShuffledIndex());
+ }
+
+ private List<MediaItem2> createAndSetPlaylist(int listSize) throws Exception {
+ List<MediaItem2> items = new ArrayList<>();
+ for (int i = 0; i < listSize; ++i) {
+ items.add(generateMediaItem(i));
+ }
+ mEventCallback.clear();
+ synchronized (mWaitLock) {
+ mAgent.setPlaylist(items, null);
+ mWaitLock.wait(WAIT_TIME_MS);
+ assertTrue(mEventCallback.onPlaylistChangedCalled);
+ }
+ return items;
+ }
+
+ private void assertPlaylistEquals(List<MediaItem2> expected, List<MediaItem2> actual) {
+ if (expected == actual) {
+ return;
+ }
+ assertTrue(expected != null && actual != null);
+ assertEquals(expected.size(), actual.size());
+ for (int i = 0; i < expected.size(); ++i) {
+ assertTrue(expected.get(i).equals(actual.get(i)));
+ }
+ }
+
+ private MediaItem2 generateMediaItemWithoutDataSourceDesc(int key) {
+ return new MediaItem2.Builder(mContext, 0)
+ .setMediaId("TEST_MEDIA_ID_WITHOUT_DSD_" + key)
+ .build();
+ }
+
+ private MediaItem2 generateMediaItem(int key) {
+ return new MediaItem2.Builder(mContext, 0)
+ .setMediaId("TEST_MEDIA_ID_" + key)
+ .build();
+ }
+}