diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_common/build.gradle b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/build.gradle new file mode 100644 index 0000000000..d225f6244b --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/build.gradle @@ -0,0 +1,21 @@ +// Copyright 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. +apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle" + +dependencies { + api 'com.google.truth:truth:' + truthVersion + implementation project(modulePrefix + 'library-common') + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + implementation 'androidx.test:core:' + androidxTestCoreVersion +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/AndroidManifest.xml b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..e92c473ecf --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/aidl/com/google/android/exoplayer2/session/vct/common/IRemoteMediaBrowserCompat.aidl b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/aidl/com/google/android/exoplayer2/session/vct/common/IRemoteMediaBrowserCompat.aidl new file mode 100644 index 0000000000..5bea101b02 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/aidl/com/google/android/exoplayer2/session/vct/common/IRemoteMediaBrowserCompat.aidl @@ -0,0 +1,37 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (String controllerId, 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.session.vct.common; + +import android.content.ComponentName; + +interface IRemoteMediaBrowserCompat { + + void create(String browserId, in ComponentName componentName); + + // MediaBrowserCompat Methods + void connect(String browserId, boolean waitForConnection); + void disconnect(String browserId); + boolean isConnected(String browserId); + ComponentName getServiceComponent(String browserId); + String getRoot(String browserId); + Bundle getExtras(String browserId); + Bundle getConnectedSessionToken(String browserId); + void subscribe(String browserId, String parentId, in Bundle options); + void unsubscribe(String browserId, String parentId); + void getItem(String browserId, String mediaId); + void search(String browserId, String query, in Bundle extras); + void sendCustomAction(String browserId, String action, in Bundle extras); +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/aidl/com/google/android/exoplayer2/session/vct/common/IRemoteMediaBrowserServiceCompat.aidl b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/aidl/com/google/android/exoplayer2/session/vct/common/IRemoteMediaBrowserServiceCompat.aidl new file mode 100644 index 0000000000..a59f5bb526 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/aidl/com/google/android/exoplayer2/session/vct/common/IRemoteMediaBrowserServiceCompat.aidl @@ -0,0 +1,22 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (String controllerId, 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.session.vct.common; + +interface IRemoteMediaBrowserServiceCompat { + + void setProxyForTest(String testName); + void notifyChildrenChanged(String parentId); +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/aidl/com/google/android/exoplayer2/session/vct/common/IRemoteMediaController.aidl b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/aidl/com/google/android/exoplayer2/session/vct/common/IRemoteMediaController.aidl new file mode 100644 index 0000000000..112032bae5 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/aidl/com/google/android/exoplayer2/session/vct/common/IRemoteMediaController.aidl @@ -0,0 +1,81 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (String controllerId, 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.session.vct.common; + +import android.net.Uri; +import android.os.ResultReceiver; + +interface IRemoteMediaController { + + void create( + boolean isBrowser, + String controllerId, + in Bundle token, + in Bundle connectionHints, + boolean waitForConnection); + + // MediaController Methods + Bundle getConnectedSessionToken(String controllerId); + void play(String controllerId); + void pause(String controllerId); + void setPlayWhenReady(String controllerId, boolean playWhenReady); + void prepare(String controllerId); + void seekToDefaultPosition(String controllerId); + void seekToDefaultPositionWithWindowIndex(String controllerId, int windowIndex); + void seekTo(String controllerId, long positionMs); + void seekToWithWindowIndex(String controllerId, int windowIndex, long positionMs); + void setPlaybackParameters(String controllerId, in Bundle playbackParametersBundle); + void setPlaybackSpeed(String controllerId, float speed); + void setMediaItems1(String controllerId, in List mediaItems, boolean resetPosition); + void setMediaItems2( + String controllerId, in List mediaItems, int startWindowIndex, long startPositionMs); + void createAndSetFakeMediaItems(String controllerId, int size); + void setMediaUri(String controllerId, in Uri uri, in Bundle extras); + void setPlaylistMetadata(String controllerId, in Bundle playlistMetadata); + void addMediaItems(String controllerId, int index, in List mediaItems); + void removeMediaItems(String controllerId, int fromIndex, int toIndex); + void moveMediaItems(String controllerId, int fromIndex, int toIndex, int newIndex); + void previous(String controllerId); + void next(String controllerId); + void setShuffleModeEnabled(String controllerId, boolean shuffleModeEnabled); + void setRepeatMode(String controllerId, int repeatMode); + void setVolumeTo(String controllerId, int value, int flags); + void adjustVolume(String controllerId, int direction, int flags); + void setVolume(String controllerId, float volume); + void setDeviceVolume(String controllerId, int volume); + void increaseDeviceVolume(String controllerId); + void decreaseDeviceVolume(String controllerId); + void setDeviceMuted(String controllerId, boolean muted); + Bundle sendCustomCommand(String controllerId, in Bundle command, in Bundle args); + Bundle setRating(String controllerId, String mediaId, in Bundle rating); + void release(String controllerId); + void stop(String controllerId); + + // MediaBrowser methods + Bundle getLibraryRoot(String controllerId, in Bundle libraryParams); + Bundle subscribe(String controllerId, String parentId, in Bundle libraryParams); + Bundle unsubscribe(String controllerId, String parentId); + Bundle getChildren( + String controllerId, + String parentId, + int page, + int pageSize, + in Bundle libraryParams); + Bundle getItem(String controllerId, String mediaId); + Bundle search(String controllerId, String query, in Bundle libraryParams); + Bundle getSearchResult( + String controllerId, String query, int page, int pageSize, in Bundle libraryParams); +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/aidl/com/google/android/exoplayer2/session/vct/common/IRemoteMediaControllerCompat.aidl b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/aidl/com/google/android/exoplayer2/session/vct/common/IRemoteMediaControllerCompat.aidl new file mode 100644 index 0000000000..0c7524df8d --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/aidl/com/google/android/exoplayer2/session/vct/common/IRemoteMediaControllerCompat.aidl @@ -0,0 +1,56 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (String controllerId, 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.session.vct.common; + +import android.net.Uri; +import android.os.ResultReceiver; + +interface IRemoteMediaControllerCompat { + + void create(String controllerId, in Bundle token, boolean waitForConnection); + + // MediaControllerCompat Methods + void addQueueItem(String controllerId, in Bundle description); + void addQueueItemWithIndex(String controllerId, in Bundle description, int index); + void removeQueueItem(String controllerId, in Bundle description); + void setVolumeTo(String controllerId, int value, int flags); + void adjustVolume(String controllerId, int direction, int flags); + void sendCommand(String controllerId, String command, in Bundle params, in ResultReceiver cb); + + // TransportControl methods + void prepare(String controllerId); + void prepareFromMediaId(String controllerId, String mediaId, in Bundle extras); + void prepareFromSearch(String controllerId, String query, in Bundle extras); + void prepareFromUri(String controllerId, in Uri uri, in Bundle extras); + void play(String controllerId); + void playFromMediaId(String controllerId, String mediaId, in Bundle extras); + void playFromSearch(String controllerId, String query, in Bundle extras); + void playFromUri(String controllerId, in Uri uri, in Bundle extras); + void skipToQueueItem(String controllerId, long id); + void pause(String controllerId); + void stop(String controllerId); + void seekTo(String controllerId, long pos); + void setPlaybackSpeed(String controllerId, float speed); + void skipToNext(String controllerId); + void skipToPrevious(String controllerId); + void setRating(String controllerId, in Bundle rating); + void setRatingWithExtras(String controllerId, in Bundle rating, in Bundle extras); + void setCaptioningEnabled(String controllerId, boolean enabled); + void setRepeatMode(String controllerId, int repeatMode); + void setShuffleMode(String controllerId, int shuffleMode); + void sendCustomAction(String controllerId, in Bundle customAction, in Bundle args); + void sendCustomActionWithName(String controllerId, String action, in Bundle args); +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/aidl/com/google/android/exoplayer2/session/vct/common/IRemoteMediaSession.aidl b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/aidl/com/google/android/exoplayer2/session/vct/common/IRemoteMediaSession.aidl new file mode 100644 index 0000000000..8adfb01003 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/aidl/com/google/android/exoplayer2/session/vct/common/IRemoteMediaSession.aidl @@ -0,0 +1,79 @@ +/* + * 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.google.android.exoplayer2.session.vct.common; + +import android.os.Bundle; +import android.os.ResultReceiver; + +interface IRemoteMediaSession { + + void create(String sessionId, in Bundle tokenExtras); + + // MediaSession Methods + Bundle getToken(String sessionId); + Bundle getCompatToken(String sessionId); + void setSessionPositionUpdateDelayMs(String sessionId, long updateDelayMs); + void setPlayer(String sessionId, in Bundle playerBundle); + void broadcastCustomCommand(String sessionId, in Bundle command, in Bundle args); + void sendCustomCommand( + String sessionId, in Bundle controller, in Bundle command, in Bundle args); + void release(String sessionId); + void setAvailableCommands(String sessionId, in Bundle controller, in Bundle sessionCommands, in Bundle playerCommands); + void setCustomLayout(String sessionId, in Bundle controller, in List layout); + + // Player Methods + void setPlayWhenReady(String sessionId, boolean playWhenReady, int reason); + void setPlaybackState(String sessionId, int state); + void setCurrentPosition(String sessionId, long pos); + void setBufferedPosition(String sessionId, long pos); + void setDuration(String sessionId, long duration); + void setBufferedPercentage(String sessionId, int bufferedPercentage); + void setTotalBufferedDuration(String sessionId, long totalBufferedDuration); + void setCurrentLiveOffset(String sessionId, long currentLiveOffset); + void setContentDuration(String sessionId, long contentDuration); + void setContentPosition(String sessionId, long contentPosition); + void setContentBufferedPosition(String sessionId, long contentBufferedPosition); + void setPlaybackParameters(String sessionId, in Bundle playbackParametersBundle); + void setIsPlayingAd(String sessionId, boolean isPlayingAd); + void setCurrentAdGroupIndex(String sessionId, int currentAdGroupIndex); + void setCurrentAdIndexInAdGroup(String sessionId, int currentAdIndexInAdGroup); + void notifyPlayerError(String sessionId, in Bundle playerErrorBundle); + void notifyPlayWhenReadyChanged(String sessionId, boolean playWhenReady, int reason); + void notifyPlaybackStateChanged(String sessionId, int state); + void notifyIsPlayingChanged(String sessionId, boolean isPlaying); + void notifyIsLoadingChanged(String sessionId, boolean isLoading); + void notifyPositionDiscontinuity(String sessionId, + in Bundle oldPositionBundle, in Bundle newPositionBundle, int reason); + void notifyPlaybackParametersChanged(String sessionId, in Bundle playbackParametersBundle); + void notifyMediaItemTransition(String sessionId, int index, int reason); + void notifyAudioAttributesChanged(String sessionId, in Bundle audioAttributes); + void notifyVideoSizeChanged(String sessionId, in Bundle videoSize); + void notifyAvailableCommandsChanged(String sessionId, in Bundle commandsBundle); + boolean surfaceExists(String sessionId); + + void setTimeline(String sessionId, in Bundle timeline); + void createAndSetFakeTimeline(String sessionId, int windowCount); + void setPlaylistMetadata(String sessionId, in Bundle metadata); + void setShuffleModeEnabled(String sessionId, boolean shuffleMode); + void setRepeatMode(String sessionId, int repeatMode); + void setCurrentWindowIndex(String sessionId, int index); + void notifyTimelineChanged(String sessionId, int reason); + void notifyPlaylistMetadataChanged(String sessionId); + void notifyShuffleModeEnabledChanged(String sessionId); + void notifyRepeatModeChanged(String sessionId); + void notifyDeviceVolumeChanged(String sessionId, int volume, boolean muted); + void notifyDeviceInfoChanged(String sessionId, in Bundle deviceInfo); +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/aidl/com/google/android/exoplayer2/session/vct/common/IRemoteMediaSessionCompat.aidl b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/aidl/com/google/android/exoplayer2/session/vct/common/IRemoteMediaSessionCompat.aidl new file mode 100644 index 0000000000..a253a2a907 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/aidl/com/google/android/exoplayer2/session/vct/common/IRemoteMediaSessionCompat.aidl @@ -0,0 +1,44 @@ +/* + * 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.google.android.exoplayer2.session.vct.common; + +import android.app.PendingIntent; +import android.os.Bundle; +import android.os.ResultReceiver; + +// Here, we use Bundle instead of the *Compat class (which implement parcelable). +// This is to avoid making dependency of testlib module on media library. +interface IRemoteMediaSessionCompat { + + void create(String sessionTag); + + // MediaSessionCompat Methods + Bundle getSessionToken(String sessionTag); + void release(String sessionTag); + void setPlaybackToLocal(String sessionTag, int stream); + void setPlaybackToRemote(String sessionTag, int volumeControl, int maxVolume, int currentVolume); + void setPlaybackState(String sessionTag, in Bundle stateBundle); + void setMetadata(String sessionTag, in Bundle metadataBundle); + void setQueue(String sessionTag, in Bundle queueBundle); + void setQueueTitle(String sessionTag, in CharSequence title); + void setRepeatMode(String sessionTag, int repeatMode); + void setShuffleMode(String sessionTag, int shuffleMode); + void setSessionActivity(String sessionTag, in PendingIntent pi); + void setFlags(String sessionTag, int flags); + void setRatingType(String sessionTag, int type); + void sendSessionEvent(String sessionTag, String event, in Bundle extras); + void setCaptioningEnabled(String sessionTag, boolean enabled); +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/CommonConstants.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/CommonConstants.java new file mode 100644 index 0000000000..e6ecb7ea72 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/CommonConstants.java @@ -0,0 +1,117 @@ +/* + * 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.google.android.exoplayer2.session.vct.common; + +import android.content.ComponentName; + +/** Common constants for testing purpose. */ +public class CommonConstants { + + public static final String SUPPORT_APP_PACKAGE_NAME = "com.google.android.exoplayer2.session.vct"; + + public static final ComponentName MEDIA2_SESSION_PROVIDER_SERVICE = + new ComponentName( + SUPPORT_APP_PACKAGE_NAME, + "com.google.android.exoplayer2.session.MediaSessionProviderService"); + public static final ComponentName MEDIA2_CONTROLLER_PROVIDER_SERVICE = + new ComponentName( + SUPPORT_APP_PACKAGE_NAME, + "com.google.android.exoplayer2.session.MediaControllerProviderService"); + + public static final ComponentName MEDIA_SESSION_COMPAT_PROVIDER_SERVICE = + new ComponentName( + SUPPORT_APP_PACKAGE_NAME, + "com.google.android.exoplayer2.session.MediaSessionCompatProviderService"); + public static final ComponentName MEDIA_CONTROLLER_COMPAT_PROVIDER_SERVICE = + new ComponentName( + SUPPORT_APP_PACKAGE_NAME, + "com.google.android.exoplayer2.session.MediaControllerCompatProviderService"); + public static final ComponentName MEDIA_BROWSER_COMPAT_PROVIDER_SERVICE = + new ComponentName( + SUPPORT_APP_PACKAGE_NAME, + "com.google.android.exoplayer2.session.MediaBrowserCompatProviderService"); + + public static final ComponentName MOCK_MEDIA2_SESSION_SERVICE = + new ComponentName( + SUPPORT_APP_PACKAGE_NAME, + "com.google.android.exoplayer2.session.MockMediaSessionService"); + public static final ComponentName MOCK_MEDIA2_LIBRARY_SERVICE = + new ComponentName( + SUPPORT_APP_PACKAGE_NAME, + "com.google.android.exoplayer2.session.MockMediaLibraryService"); + public static final ComponentName MOCK_MEDIA_BROWSER_SERVICE_COMPAT = + new ComponentName( + SUPPORT_APP_PACKAGE_NAME, + "com.google.android.exoplayer2.session.MockMediaBrowserServiceCompat"); + + public static final String ACTION_MEDIA2_SESSION = + "com.google.android.exoplayer2.session.vct.action.MEDIA2_SESSION"; + public static final String ACTION_MEDIA2_CONTROLLER = + "com.google.android.exoplayer2.session.vct.action.MEDIA2_CONTROLLER"; + public static final String ACTION_MEDIA_SESSION_COMPAT = + "com.google.android.exoplayer2.session.vct.action.MEDIA_SESSION_COMPAT"; + public static final String ACTION_MEDIA_CONTROLLER_COMPAT = + "com.google.android.exoplayer2.session.vct.action.MEDIA_CONTROLLER_COMPAT"; + public static final String ACTION_MEDIA_BROWSER_COMPAT = + "com.google.android.exoplayer2.session.vct.action.MEDIA_BROWSER_COMPAT"; + + // Keys for arguments. + public static final String KEY_PLAYER_ERROR = "playerError"; + public static final String KEY_AUDIO_ATTRIBUTES = "audioAttributes"; + public static final String KEY_TIMELINE = "timeline"; + public static final String KEY_CURRENT_WINDOW_INDEX = "currentWindowIndex"; + public static final String KEY_CURRENT_PERIOD_INDEX = "currentPeriodIndex"; + public static final String KEY_DURATION = "duration"; + public static final String KEY_CURRENT_POSITION = "currentPosition"; + public static final String KEY_BUFFERED_POSITION = "bufferedPosition"; + public static final String KEY_BUFFERED_PERCENTAGE = "bufferedPercentage"; + public static final String KEY_TOTAL_BUFFERED_DURATION = "totalBufferedDuration"; + public static final String KEY_CURRENT_LIVE_OFFSET = "currentLiveOffset"; + public static final String KEY_CONTENT_DURATION = "contentDuration"; + public static final String KEY_CONTENT_POSITION = "contentPosition"; + public static final String KEY_CONTENT_BUFFERED_POSITION = "contentBufferedPosition"; + public static final String KEY_PLAYBACK_PARAMETERS = "playbackParameters"; + public static final String KEY_MEDIA_ITEM = "mediaItem"; + public static final String KEY_PLAYLIST_METADATA = "playlistMetadata"; + public static final String KEY_ARGUMENTS = "arguments"; + public static final String KEY_DEVICE_INFO = "deviceInfo"; + public static final String KEY_DEVICE_VOLUME = "deviceVolume"; + public static final String KEY_DEVICE_MUTED = "deviceMuted"; + public static final String KEY_VIDEO_SIZE = "videoSize"; + public static final String KEY_VOLUME = "volume"; + public static final String KEY_PLAY_WHEN_READY = "playWhenReady"; + public static final String KEY_PLAYBACK_SUPPRESSION_REASON = "playbackSuppressionReason"; + public static final String KEY_PLAYBACK_STATE = "playbackState"; + public static final String KEY_IS_PLAYING = "isPlaying"; + public static final String KEY_IS_LOADING = "isLoading"; + public static final String KEY_REPEAT_MODE = "repeatMode"; + public static final String KEY_SHUFFLE_MODE_ENABLED = "shuffleModeEnabled"; + public static final String KEY_IS_PLAYING_AD = "isPlayingAd"; + public static final String KEY_CURRENT_AD_GROUP_INDEX = "currentAdGroupIndex"; + public static final String KEY_CURRENT_AD_INDEX_IN_AD_GROUP = "currentAdIndexInAdGroup"; + + // SessionCompat arguments + public static final String KEY_SESSION_COMPAT_TOKEN = "sessionCompatToken"; + public static final String KEY_PLAYBACK_STATE_COMPAT = "playbackStateCompat"; + public static final String KEY_METADATA_COMPAT = "metadataCompat"; + public static final String KEY_QUEUE = "queue"; + + // Default test name + public static final String DEFAULT_TEST_NAME = "defaultTestName"; + + private CommonConstants() {} +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/CustomParcelable.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/CustomParcelable.java new file mode 100644 index 0000000000..09d9fa6f4e --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/CustomParcelable.java @@ -0,0 +1,57 @@ +/* + * Copyright 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. + */ + +package com.google.android.exoplayer2.session.vct.common; + +import android.annotation.SuppressLint; +import android.os.Parcel; +import android.os.Parcelable; + +/** Custom Parcelable class to test sending/receiving user parcelables between processes. */ +@SuppressLint("BanParcelableUsage") +public class CustomParcelable implements Parcelable { + + private int value; + + public CustomParcelable(int value) { + this.value = value; + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressLint("UnknownNullness") // Parcel dest + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(value); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public CustomParcelable createFromParcel(Parcel in) { + int value = in.readInt(); + return new CustomParcelable(value); + } + + @Override + public CustomParcelable[] newArray(int size) { + return new CustomParcelable[size]; + } + }; +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/HandlerThreadTestRule.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/HandlerThreadTestRule.java new file mode 100644 index 0000000000..9ad528f19f --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/HandlerThreadTestRule.java @@ -0,0 +1,73 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.session.vct.common; + +import android.os.Build; +import android.os.HandlerThread; +import java.util.concurrent.Executor; +import org.junit.rules.ExternalResource; + +/** TestRule for providing a handler and an executor for {@link HandlerThread}. */ +public final class HandlerThreadTestRule extends ExternalResource { + + private final String threadName; + private TestHandler handler; + private Executor executor; + + public HandlerThreadTestRule(String threadName) { + this.threadName = threadName; + } + + @Override + protected void before() { + HandlerThread handlerThread = new HandlerThread(threadName); + handlerThread.start(); + + TestHandler handler = new TestHandler(handlerThread.getLooper()); + executor = handler::post; + this.handler = handler; + } + + @Override + protected void after() { + try { + if (Build.VERSION.SDK_INT >= 18) { + handler.getLooper().quitSafely(); + } else { + handler.getLooper().quit(); + } + } finally { + handler = null; + executor = null; + } + } + + /** Gets the handler for the thread. */ + public TestHandler getHandler() { + if (handler == null) { + throw new IllegalStateException("It should be called between before() and after()"); + } + return handler; + } + + /** Gets the executor that executes the commands on the thread. */ + public Executor getExecutor() { + if (executor == null) { + throw new IllegalStateException("It should be called between before() and after()"); + } + return executor; + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/MainLooperTestRule.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/MainLooperTestRule.java new file mode 100644 index 0000000000..6e330ca9fa --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/MainLooperTestRule.java @@ -0,0 +1,61 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.session.vct.common; + +import android.content.Context; +import android.media.AudioManager; +import android.os.Looper; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +/** TestRule for preparing main looper. */ +public final class MainLooperTestRule implements TestRule { + + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + prepare(); + base.evaluate(); + } + }; + } + + private void prepare() { + InstrumentationRegistry.getInstrumentation() + .runOnMainSync( + () -> { + // Prepare the main looper if it hasn't. + // Some framework APIs always run on the main looper. + if (Looper.getMainLooper() == null) { + Looper.prepareMainLooper(); + } + + // Initialize AudioManager on the main thread to workaround b/78617702 that + // audio focus listener is called on the thread where the AudioManager was + // originally initialized. + // Without posting this, audio focus listeners wouldn't be called because the + // listeners would be posted to the test thread (here) where it waits until the + // tests are finished. + Context context = ApplicationProvider.getApplicationContext(); + AudioManager manager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + }); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/MediaBrowserConstants.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/MediaBrowserConstants.java new file mode 100644 index 0000000000..b8e3318397 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/MediaBrowserConstants.java @@ -0,0 +1,87 @@ +/* + * 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.google.android.exoplayer2.session.vct.common; + +import android.os.Bundle; +import java.util.ArrayList; +import java.util.List; + +/** Constants for calling MediaBrowser methods. */ +public class MediaBrowserConstants { + + public static final String ROOT_ID = "rootId"; + public static final Bundle ROOT_EXTRAS = new Bundle(); + + public static final String MEDIA_ID_GET_ITEM = "media_id_get_item"; + public static final String MEDIA_ID_GET_NULL_ITEM = "media_id_get_null_item"; + + public static final String PARENT_ID = "parent_id"; + public static final String PARENT_ID_LONG_LIST = "parent_id_long_list"; + public static final String PARENT_ID_NO_CHILDREN = "parent_id_no_children"; + public static final String PARENT_ID_ERROR = "parent_id_error"; + + public static final List GET_CHILDREN_RESULT = new ArrayList<>(); + public static final int CHILDREN_COUNT = 100; + + public static final int LONG_LIST_COUNT = 5000; + + public static final String SEARCH_QUERY = "search_query"; + public static final String SEARCH_QUERY_LONG_LIST = "search_query_long_list"; + public static final String SEARCH_QUERY_TAKES_TIME = "search_query_takes_time"; + public static final long SEARCH_TIME_IN_MS = 5_000; + public static final String SEARCH_QUERY_EMPTY_RESULT = "search_query_empty_result"; + public static final String SEARCH_QUERY_ERROR = "search_query_error"; + + public static final List SEARCH_RESULT = new ArrayList<>(); + public static final int SEARCH_RESULT_COUNT = 50; + + public static final String SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ALL = + "subscribe_id_notify_children_changed_to_all"; + public static final String SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE = + "subscribe_id_notify_children_changed_to_one"; + public static final String SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ALL_WITH_NON_SUBSCRIBED_ID = + "subscribe_id_notify_children_changed_to_all_with_non_subscribed_id"; + public static final String SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE_WITH_NON_SUBSCRIBED_ID = + "subscribe_id_notify_children_changed_to_one_with_non_subscribed_id"; + public static final int NOTIFY_CHILDREN_CHANGED_ITEM_COUNT = 101; + public static final Bundle NOTIFY_CHILDREN_CHANGED_EXTRAS = TestUtils.createTestBundle(); + + public static final String CUSTOM_ACTION = "customAction"; + public static final Bundle CUSTOM_ACTION_EXTRAS = new Bundle(); + + public static final String CUSTOM_ACTION_ASSERT_PARAMS = "assertParams"; + + static { + ROOT_EXTRAS.putString(ROOT_ID, ROOT_ID); + + CUSTOM_ACTION_EXTRAS.putString(CUSTOM_ACTION, CUSTOM_ACTION); + + GET_CHILDREN_RESULT.clear(); + String getChildrenMediaIdPrefix = "get_children_media_id_"; + for (int i = 0; i < CHILDREN_COUNT; i++) { + GET_CHILDREN_RESULT.add(getChildrenMediaIdPrefix + i); + } + + SEARCH_RESULT.clear(); + String getSearchResultMediaIdPrefix = "get_search_result_media_id_"; + for (int i = 0; i < SEARCH_RESULT_COUNT; i++) { + SEARCH_RESULT.add(getSearchResultMediaIdPrefix + i); + } + } + + private MediaBrowserConstants() {} +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/MediaBrowserServiceCompatConstants.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/MediaBrowserServiceCompatConstants.java new file mode 100644 index 0000000000..1cc575423f --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/MediaBrowserServiceCompatConstants.java @@ -0,0 +1,26 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.session.vct.common; + +/** Constants for calling MediaBrowserServiceCompat methods. */ +public class MediaBrowserServiceCompatConstants { + + public static final String TEST_CONNECT_REJECTED = "testConnect_rejected"; + public static final String TEST_ON_CHILDREN_CHANGED_SUBSCRIBE_AND_UNSUBSCRIBE = + "testOnChildrenChanged_subscribeAndUnsubscribe"; + + private MediaBrowserServiceCompatConstants() {} +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/MediaSessionConstants.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/MediaSessionConstants.java new file mode 100644 index 0000000000..b4bb8682f8 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/MediaSessionConstants.java @@ -0,0 +1,28 @@ +/* + * 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.google.android.exoplayer2.session.vct.common; + +/** Constants for calling MediaSession methods. */ +public class MediaSessionConstants { + + // Test method names + public static final String TEST_GET_SESSION_ACTIVITY = "testGetSessionActivity"; + public static final String TEST_CONTROLLER_CALLBACK_SESSION_REJECTS = + "testControllerCallback_sessionRejects"; + + private MediaSessionConstants() {} +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/MockActivity.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/MockActivity.java new file mode 100644 index 0000000000..4ae88bf540 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/MockActivity.java @@ -0,0 +1,22 @@ +/* + * 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.google.android.exoplayer2.session.vct.common; + +import android.app.Activity; + +/** An empty activity used for testing. */ +public class MockActivity extends Activity {} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/PollingCheck.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/PollingCheck.java new file mode 100644 index 0000000000..b2aa8a5b80 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/PollingCheck.java @@ -0,0 +1,81 @@ +/* + * 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.google.android.exoplayer2.session.vct.common; + +import static com.google.common.truth.Truth.assertWithMessage; + +import androidx.annotation.NonNull; + +/** + * Utility used for testing that allows to poll for a certain condition to happen within a timeout. + */ +// It's forked from androidx.testutils.PollingCheck. +public abstract class PollingCheck { + private static final long TIME_SLICE_MS = 50; + private final long timeoutMs; + + /** The condition that the PollingCheck should use to proceed successfully. */ + public interface PollingCheckCondition { + /** @return Whether the polling condition has been met. */ + boolean canProceed() throws Exception; + } + + private PollingCheck(long timeoutMs) { + this.timeoutMs = timeoutMs; + } + + protected abstract boolean check() throws Exception; + + /** Start running the polling check. */ + public void run() throws Exception { + if (check()) { + return; + } + + long timeoutMs = this.timeoutMs; + while (timeoutMs > 0) { + try { + Thread.sleep(TIME_SLICE_MS); + } catch (InterruptedException e) { + throw new AssertionError("unexpected InterruptedException"); + } + + if (check()) { + return; + } + + timeoutMs -= TIME_SLICE_MS; + } + + assertWithMessage("unexpected timeout").fail(); + } + + /** + * Instantiate and start polling for a given condition. + * + * @param timeoutMs Timeout in milliseconds. + * @param condition The condition to check for success. + */ + public static void waitFor(long timeoutMs, @NonNull PollingCheckCondition condition) + throws Exception { + new PollingCheck(timeoutMs) { + @Override + protected boolean check() throws Exception { + return condition.canProceed(); + } + }.run(); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/SurfaceActivity.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/SurfaceActivity.java new file mode 100644 index 0000000000..b2aa54529d --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/SurfaceActivity.java @@ -0,0 +1,49 @@ +/* + * Copyright 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. + */ + +package com.google.android.exoplayer2.session.vct.common; + +import android.app.Activity; +import android.os.Bundle; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +/** An activity used for surface test */ +public class SurfaceActivity extends Activity { + private SurfaceHolder firstSurfaceHolder; + private SurfaceHolder secondSurfaceHolder; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + TestUtils.setKeepScreenOn(this); + setContentView(R.layout.activity_surface); + + SurfaceView firstSurfaceView = findViewById(R.id.surface_view_first); + firstSurfaceHolder = firstSurfaceView.getHolder(); + + SurfaceView secondSurfaceView = findViewById(R.id.surface_view_second); + secondSurfaceHolder = secondSurfaceView.getHolder(); + } + + public SurfaceHolder getFirstSurfaceHolder() { + return firstSurfaceHolder; + } + + public SurfaceHolder getSecondSurfaceHolder() { + return secondSurfaceHolder; + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/TestHandler.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/TestHandler.java new file mode 100644 index 0000000000..ada1113de1 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/TestHandler.java @@ -0,0 +1,106 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.session.vct.common; + +import static com.google.android.exoplayer2.session.vct.common.TestUtils.LONG_TIMEOUT_MS; +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.NonNull; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +/** Handler for testing. */ +public class TestHandler extends Handler { + + private static final long DEFAULT_TIMEOUT_MS = LONG_TIMEOUT_MS; + + public TestHandler(@NonNull Looper looper) { + super(looper); + } + + /** Posts {@link Runnable} and waits until it finishes, or runs it directly on the same looper. */ + public void postAndSync(@NonNull TestRunnable runnable) throws Exception { + postAndSync(runnable, DEFAULT_TIMEOUT_MS); + } + + /** Posts {@link Runnable} and waits until it finishes, or runs it directly on the same looper. */ + public void postAndSync(@NonNull TestRunnable runnable, long timeoutMs) throws Exception { + if (getLooper() == Looper.myLooper()) { + runnable.run(); + } else { + AtomicReference exception = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + post( + () -> { + try { + runnable.run(); + } catch (Exception e) { + exception.set(e); + } + latch.countDown(); + }); + assertThat(latch.await(timeoutMs, MILLISECONDS)).isTrue(); + if (exception.get() != null) { + throw exception.get(); + } + } + } + + /** + * Posts {@link Callable} and returns the result when it finishes, or calls it directly on the + * same looper. + */ + public V postAndSync(@NonNull Callable callable) throws Exception { + return postAndSync(callable, DEFAULT_TIMEOUT_MS); + } + + /** + * Posts {@link Callable} and returns the result when it finishes, or calls it directly on the + * same looper. + */ + public V postAndSync(@NonNull Callable callable, long timeoutMs) throws Exception { + if (getLooper() == Looper.myLooper()) { + return callable.call(); + } else { + AtomicReference result = new AtomicReference<>(); + AtomicReference exception = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + post( + () -> { + try { + result.set(callable.call()); + } catch (Exception e) { + exception.set(e); + } + latch.countDown(); + }); + assertThat(latch.await(timeoutMs, MILLISECONDS)).isTrue(); + if (exception.get() != null) { + throw exception.get(); + } + return result.get(); + } + } + + /** {@link Runnable} variant which can throw a checked exception. */ + public interface TestRunnable { + void run() throws Exception; + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/TestUtils.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/TestUtils.java new file mode 100644 index 0000000000..d281e88ac6 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/java/com/google/android/exoplayer2/session/vct/common/TestUtils.java @@ -0,0 +1,131 @@ +/* + * 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.google.android.exoplayer2.session.vct.common; + +import static android.content.Context.KEYGUARD_SERVICE; + +import android.app.Activity; +import android.app.KeyguardManager; +import android.os.Build; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.WindowManager; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; +import com.google.android.exoplayer2.util.Util; +import java.util.Locale; + +/** Provides utility methods for testing purpose. */ +public class TestUtils { + + public static final long TIMEOUT_MS = 5_000; + public static final long NO_RESPONSE_TIMEOUT_MS = 500; + public static final long SERVICE_CONNECTION_TIMEOUT_MS = 3_000; + public static final long VOLUME_CHANGE_TIMEOUT_MS = 5_000; + public static final long LONG_TIMEOUT_MS = 10_000; + + /** + * Compares contents of two throwables for both message and class. + * + * @param a a throwable + * @param b another throwable + * @return {@code true} if two throwables are the same class and same messages. {@code false} + * otherwise. + */ + public static boolean equals(@Nullable Throwable a, @Nullable Throwable b) { + if (a == null || b == null) { + return a == b; + } + return a.getClass() == b.getClass() && TextUtils.equals(a.getMessage(), b.getMessage()); + } + + /** + * Compares contents of two bundles. + * + * @param a a bundle + * @param b another bundle + * @return {@code true} if two bundles are the same. {@code false} otherwise. This may be + * incorrect if any bundle contains a bundle. + */ + public static boolean equals(Bundle a, Bundle b) { + return contains(a, b) && contains(b, a); + } + + /** + * Checks whether a Bundle contains another bundle. + * + * @param a a bundle + * @param b another bundle + * @return {@code true} if a contains b. {@code false} otherwise. This may be incorrect if any + * bundle contains a bundle. + */ + public static boolean contains(Bundle a, Bundle b) { + if (a == b) { + return true; + } + if (a == null || b == null) { + return b == null; + } + if (!a.keySet().containsAll(b.keySet())) { + return false; + } + for (String key : b.keySet()) { + if (!Util.areEqual(a.get(key), b.get(key))) { + return false; + } + } + return true; + } + + /** + * Create a bundle for testing purpose. + * + * @return the newly created bundle. + */ + public static Bundle createTestBundle() { + Bundle bundle = new Bundle(); + bundle.putString("test_key", "test_value"); + return bundle; + } + + /** Gets the expected mediaId for the windowIndex when testing with a fake timeline. */ + public static String getMediaIdInFakeTimeline(int windowIndex) { + return String.format(Locale.US, "%08d", windowIndex); + } + + @UiThread + static void setKeepScreenOn(Activity activity) { + if (Build.VERSION.SDK_INT >= 27) { + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + activity.setTurnScreenOn(true); + activity.setShowWhenLocked(true); + KeyguardManager keyguardManager = + (KeyguardManager) activity.getSystemService(KEYGUARD_SERVICE); + keyguardManager.requestDismissKeyguard(activity, null); + } else { + activity + .getWindow() + .addFlags( + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); + } + } + + private TestUtils() {} +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/res/drawable/big_buck_bunny.jpg b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/res/drawable/big_buck_bunny.jpg new file mode 100644 index 0000000000..f29d12062c Binary files /dev/null and b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/res/drawable/big_buck_bunny.jpg differ diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/res/layout/activity_surface.xml b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/res/layout/activity_surface.xml new file mode 100644 index 0000000000..4f132ce5fc --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/res/layout/activity_surface.xml @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/res/raw/camera_click.ogg b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/res/raw/camera_click.ogg new file mode 100644 index 0000000000..b836e10948 Binary files /dev/null and b/google3/third_party/java_src/android_libs/media/libraries/test_session_common/src/main/res/raw/camera_click.ogg differ diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/build.gradle b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/build.gradle new file mode 100644 index 0000000000..a115e4b35b --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/build.gradle @@ -0,0 +1,45 @@ +// Copyright 2021 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. +apply from: "$gradle.ext.exoplayerSettingsDir/constants.gradle" +apply plugin: 'com.android.application' + +android { + compileSdkVersion project.ext.compileSdkVersion + + defaultConfig { + versionName project.ext.releaseVersion + versionCode project.ext.releaseVersionCode + minSdkVersion project.ext.minSdkVersion + targetSdkVersion project.ext.appTargetSdkVersion + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +// TODO(b/178560255): Remove explicit "group" after moving to androidx package. +group 'androidx.media3' + +dependencies { + implementation project(modulePrefix + 'library-session') + implementation project(modulePrefix + 'test-session-common') + androidTestImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion + androidTestImplementation 'androidx.test.ext:truth:' + androidxTestTruthVersion + androidTestImplementation 'androidx.test:core:' + androidxTestCoreVersion + androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion + androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/AndroidManifest.xml b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000000..5fbdd4582a --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaBrowserCallbackTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaBrowserCallbackTest.java new file mode 100644 index 0000000000..54c44cdbe4 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaBrowserCallbackTest.java @@ -0,0 +1,447 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.LibraryResult.RESULT_ERROR_BAD_VALUE; +import static com.google.android.exoplayer2.session.LibraryResult.RESULT_SUCCESS; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.MOCK_MEDIA2_LIBRARY_SERVICE; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.CUSTOM_ACTION_ASSERT_PARAMS; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.LONG_LIST_COUNT; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED_EXTRAS; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED_ITEM_COUNT; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.ROOT_EXTRAS; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.ROOT_ID; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ALL; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ALL_WITH_NON_SUBSCRIBED_ID; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE_WITH_NON_SUBSCRIBED_ID; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.LONG_TIMEOUT_MS; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.NO_RESPONSE_TIMEOUT_MS; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.TIMEOUT_MS; +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.os.Bundle; +import android.os.RemoteException; +import androidx.annotation.Nullable; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import com.google.android.exoplayer2.session.MediaBrowser.BrowserCallback; +import com.google.android.exoplayer2.session.MediaLibraryService.LibraryParams; +import com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants; +import com.google.android.exoplayer2.session.vct.common.TestUtils; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeoutException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for {@link MediaBrowser.BrowserCallback}. + * + *

This test inherits {@link MediaControllerCallbackTest} to ensure that inherited APIs from + * {@link MediaController} works cleanly. + */ +// TODO: (internal cleanup) Move tests that aren't related with callbacks. +@RunWith(AndroidJUnit4.class) +@LargeTest +public class MediaBrowserCallbackTest extends MediaControllerCallbackTest { + private static final String TAG = "MediaBrowserCallbackTest"; + + @Before + public void setControllerType() { + controllerTestRule.setControllerType(MediaBrowser.class); + } + + private MediaBrowser createBrowser() throws Exception { + return createBrowser(null, null); + } + + private MediaBrowser createBrowser( + @Nullable Bundle connectionHints, @Nullable BrowserCallback callback) throws Exception { + SessionToken token = new SessionToken(context, MOCK_MEDIA2_LIBRARY_SERVICE); + return (MediaBrowser) + controllerTestRule.createController(token, true, connectionHints, callback); + } + + @Test + public void getLibraryRoot() throws Exception { + LibraryParams params = + new LibraryParams.Builder() + .setOffline(true) + .setRecent(true) + .setExtras(new Bundle()) + .build(); + + MediaBrowser browser = createBrowser(); + setExpectedLibraryParam(browser, params); + LibraryResult result = browser.getLibraryRoot(params).get(TIMEOUT_MS, MILLISECONDS); + assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); + assertThat(result.item.mediaId).isEqualTo(ROOT_ID); + assertThat(TestUtils.equals(ROOT_EXTRAS, result.params.extras)).isTrue(); + } + + @Test + public void getItem() throws Exception { + String mediaId = MediaBrowserConstants.MEDIA_ID_GET_ITEM; + + LibraryResult result = createBrowser().getItem(mediaId).get(TIMEOUT_MS, MILLISECONDS); + assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); + assertThat(result.item.mediaId).isEqualTo(mediaId); + } + + @Test + public void getItem_unknownId() throws Exception { + String mediaId = "random_media_id"; + + LibraryResult result = createBrowser().getItem(mediaId).get(TIMEOUT_MS, MILLISECONDS); + assertThat(result.resultCode).isEqualTo(RESULT_ERROR_BAD_VALUE); + assertThat(result.item).isNull(); + } + + @Test + public void getItem_nullResult() throws Exception { + String mediaId = MediaBrowserConstants.MEDIA_ID_GET_NULL_ITEM; + + // Exception will be thrown in the service side, and the process will be crashed. + // In that case one of following will happen + // Case 1) Process is crashed. Pending ListenableFuture will get error + // Case 2) Due to the frequent crashes with other tests, process may not crash immediately + // because the Android shows dialog 'xxx keeps stopping' and defer sending + // SIG_KILL until the user's explicit action. + try { + LibraryResult result = createBrowser().getItem(mediaId).get(TIMEOUT_MS, MILLISECONDS); + // Case 1. + assertThat(result.resultCode).isNotEqualTo(RESULT_SUCCESS); + } catch (TimeoutException e) { + // Case 2. + } + + // Clean up RemoteMediaSession proactively to avoid crash at cleanUp() + try { + remoteSession.cleanUp(); + } catch (RemoteException e) { + // Expected + } + remoteSession = null; + } + + @Test + public void getChildren() throws Exception { + String parentId = MediaBrowserConstants.PARENT_ID; + int page = 4; + int pageSize = 10; + LibraryParams params = MediaTestUtils.createLibraryParams(); + + MediaBrowser browser = createBrowser(); + setExpectedLibraryParam(browser, params); + + LibraryResult result = + browser.getChildren(parentId, page, pageSize, params).get(TIMEOUT_MS, MILLISECONDS); + assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); + assertThat(result.params).isNull(); + + MediaTestUtils.assertPaginatedListHasIds( + result.items, MediaBrowserConstants.GET_CHILDREN_RESULT, page, pageSize); + } + + @Test + @LargeTest + public void getChildren_withLongList() throws Exception { + String parentId = MediaBrowserConstants.PARENT_ID_LONG_LIST; + int page = 0; + int pageSize = Integer.MAX_VALUE; + LibraryParams params = MediaTestUtils.createLibraryParams(); + + MediaBrowser browser = createBrowser(); + setExpectedLibraryParam(browser, params); + + LibraryResult result = + browser.getChildren(parentId, page, pageSize, params).get(LONG_TIMEOUT_MS, MILLISECONDS); + assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); + assertThat(result.params).isNull(); + + assertThat(result.items).hasSize(LONG_LIST_COUNT); + for (int i = 0; i < result.items.size(); i++) { + assertThat(result.items.get(i).mediaId).isEqualTo(TestUtils.getMediaIdInFakeTimeline(i)); + } + } + + @Test + public void getChildren_emptyResult() throws Exception { + String parentId = MediaBrowserConstants.PARENT_ID_NO_CHILDREN; + + MediaBrowser browser = createBrowser(); + LibraryResult result = browser.getChildren(parentId, 1, 1, null).get(TIMEOUT_MS, MILLISECONDS); + assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); + assertThat(result.items.size()).isEqualTo(0); + } + + @Test + public void getChildren_nullResult() throws Exception { + String parentId = MediaBrowserConstants.PARENT_ID_ERROR; + + MediaBrowser browser = createBrowser(); + LibraryResult result = browser.getChildren(parentId, 1, 1, null).get(TIMEOUT_MS, MILLISECONDS); + assertThat(result.resultCode).isNotEqualTo(RESULT_SUCCESS); + assertThat(result.items).isNull(); + } + + @Test + public void searchCallbacks() throws Exception { + String query = MediaBrowserConstants.SEARCH_QUERY; + int page = 4; + int pageSize = 10; + LibraryParams testParams = MediaTestUtils.createLibraryParams(); + + CountDownLatch latchForSearch = new CountDownLatch(1); + BrowserCallback callback = + new BrowserCallback() { + @Override + public void onSearchResultChanged( + MediaBrowser browser, String queryOut, int itemCount, LibraryParams params) { + assertThat(queryOut).isEqualTo(query); + MediaTestUtils.assertLibraryParamsEquals(testParams, params); + assertThat(itemCount).isEqualTo(MediaBrowserConstants.SEARCH_RESULT_COUNT); + latchForSearch.countDown(); + } + }; + + // Request the search. + MediaBrowser browser = createBrowser(null, callback); + setExpectedLibraryParam(browser, testParams); + LibraryResult result = browser.search(query, testParams).get(TIMEOUT_MS, MILLISECONDS); + assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); + + // Get the search result. + result = + browser.getSearchResult(query, page, pageSize, testParams).get(TIMEOUT_MS, MILLISECONDS); + assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); + MediaTestUtils.assertPaginatedListHasIds( + result.items, MediaBrowserConstants.SEARCH_RESULT, page, pageSize); + } + + @Test + @LargeTest + public void searchCallbacks_withLongList() throws Exception { + String query = MediaBrowserConstants.SEARCH_QUERY_LONG_LIST; + int page = 0; + int pageSize = Integer.MAX_VALUE; + LibraryParams testParams = MediaTestUtils.createLibraryParams(); + + CountDownLatch latch = new CountDownLatch(1); + BrowserCallback callback = + new BrowserCallback() { + @Override + public void onSearchResultChanged( + MediaBrowser browser, String queryOut, int itemCount, LibraryParams params) { + assertThat(queryOut).isEqualTo(query); + MediaTestUtils.assertLibraryParamsEquals(testParams, params); + assertThat(itemCount).isEqualTo(MediaBrowserConstants.LONG_LIST_COUNT); + latch.countDown(); + } + }; + + MediaBrowser browser = createBrowser(null, callback); + setExpectedLibraryParam(browser, testParams); + LibraryResult result = browser.search(query, testParams).get(TIMEOUT_MS, MILLISECONDS); + assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); + + result = + browser + .getSearchResult(query, page, pageSize, testParams) + .get(LONG_TIMEOUT_MS, MILLISECONDS); + assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); + for (int i = 0; i < result.items.size(); i++) { + assertThat(result.items.get(i).mediaId).isEqualTo(TestUtils.getMediaIdInFakeTimeline(i)); + } + } + + @Test + @LargeTest + public void onSearchResultChanged_searchTakesTime() throws Exception { + String query = MediaBrowserConstants.SEARCH_QUERY_TAKES_TIME; + LibraryParams testParams = MediaTestUtils.createLibraryParams(); + + CountDownLatch latch = new CountDownLatch(1); + BrowserCallback callback = + new BrowserCallback() { + @Override + public void onSearchResultChanged( + MediaBrowser browser, String queryOut, int itemCount, LibraryParams params) { + assertThat(queryOut).isEqualTo(query); + MediaTestUtils.assertLibraryParamsEquals(testParams, params); + assertThat(itemCount).isEqualTo(MediaBrowserConstants.SEARCH_RESULT_COUNT); + latch.countDown(); + } + }; + + MediaBrowser browser = createBrowser(null, callback); + setExpectedLibraryParam(browser, testParams); + LibraryResult result = + browser + .search(query, testParams) + .get(MediaBrowserConstants.SEARCH_TIME_IN_MS + TIMEOUT_MS, MILLISECONDS); + assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); + } + + @Test + public void onSearchResultChanged_emptyResult() throws Exception { + String query = MediaBrowserConstants.SEARCH_QUERY_EMPTY_RESULT; + LibraryParams testParams = MediaTestUtils.createLibraryParams(); + + CountDownLatch latch = new CountDownLatch(1); + BrowserCallback callback = + new BrowserCallback() { + @Override + public void onSearchResultChanged( + MediaBrowser browser, String queryOut, int itemCount, LibraryParams params) { + assertThat(queryOut).isEqualTo(query); + MediaTestUtils.assertLibraryParamsEquals(testParams, params); + assertThat(itemCount).isEqualTo(0); + latch.countDown(); + } + }; + + MediaBrowser browser = createBrowser(null, callback); + setExpectedLibraryParam(browser, testParams); + LibraryResult result = browser.search(query, testParams).get(TIMEOUT_MS, MILLISECONDS); + assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); + } + + @Test + public void onChildrenChanged_calledWhenSubscribed() throws Exception { + // This test uses MediaLibrarySession.notifyChildrenChanged(). + String expectedParentId = SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ALL; + + CountDownLatch latch = new CountDownLatch(1); + BrowserCallback controllerCallbackProxy = + new BrowserCallback() { + @Override + public void onChildrenChanged( + MediaBrowser browser, String parentId, int itemCount, LibraryParams params) { + assertThat(parentId).isEqualTo(expectedParentId); + assertThat(itemCount).isEqualTo(NOTIFY_CHILDREN_CHANGED_ITEM_COUNT); + MediaTestUtils.assertLibraryParamsEquals(params, NOTIFY_CHILDREN_CHANGED_EXTRAS); + latch.countDown(); + } + }; + + LibraryResult result = + createBrowser(null, controllerCallbackProxy) + .subscribe(expectedParentId, null) + .get(TIMEOUT_MS, MILLISECONDS); + assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); + + // The MediaLibrarySession in MockMediaLibraryService is supposed to call + // notifyChildrenChanged() in its callback onSubscribe(). + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void onChildrenChanged_calledWhenSubscribed2() throws Exception { + // This test uses MediaLibrarySession.notifyChildrenChanged(ControllerInfo). + String expectedParentId = SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE; + + CountDownLatch latch = new CountDownLatch(1); + BrowserCallback controllerCallbackProxy = + new BrowserCallback() { + @Override + public void onChildrenChanged( + MediaBrowser browser, String parentId, int itemCount, LibraryParams params) { + assertThat(parentId).isEqualTo(expectedParentId); + assertThat(itemCount).isEqualTo(NOTIFY_CHILDREN_CHANGED_ITEM_COUNT); + MediaTestUtils.assertLibraryParamsEquals(params, NOTIFY_CHILDREN_CHANGED_EXTRAS); + latch.countDown(); + } + }; + + LibraryResult result = + createBrowser(null, controllerCallbackProxy) + .subscribe(expectedParentId, null) + .get(TIMEOUT_MS, MILLISECONDS); + assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); + + // The MediaLibrarySession in MockMediaLibraryService is supposed to call + // notifyChildrenChanged(ControllerInfo) in its callback onSubscribe(). + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void onChildrenChanged_notCalledWhenNotSubscribed() throws Exception { + // This test uses MediaLibrarySession.notifyChildrenChanged(). + String subscribedMediaId = SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ALL_WITH_NON_SUBSCRIBED_ID; + CountDownLatch latch = new CountDownLatch(1); + + BrowserCallback controllerCallbackProxy = + new BrowserCallback() { + @Override + public void onChildrenChanged( + MediaBrowser browser, String parentId, int itemCount, LibraryParams params) { + latch.countDown(); + } + }; + + LibraryResult result = + createBrowser(null, controllerCallbackProxy) + .subscribe(subscribedMediaId, null) + .get(TIMEOUT_MS, MILLISECONDS); + assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); + + // The MediaLibrarySession in MockMediaLibraryService is supposed to call + // notifyChildrenChanged() in its callback onSubscribe(), but with a different media ID. + // Therefore, onChildrenChanged() should not be called. + assertThat(latch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); + } + + @Test + public void onChildrenChanged_notCalledWhenNotSubscribed2() throws Exception { + // This test uses MediaLibrarySession.notifyChildrenChanged(ControllerInfo). + String subscribedMediaId = SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE_WITH_NON_SUBSCRIBED_ID; + CountDownLatch latch = new CountDownLatch(1); + + BrowserCallback controllerCallbackProxy = + new BrowserCallback() { + @Override + public void onChildrenChanged( + MediaBrowser browser, String parentId, int itemCount, LibraryParams params) { + latch.countDown(); + } + }; + + LibraryResult result = + createBrowser(null, controllerCallbackProxy) + .subscribe(subscribedMediaId, null) + .get(TIMEOUT_MS, MILLISECONDS); + assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); + + // The MediaLibrarySession in MockMediaLibraryService is supposed to call + // notifyChildrenChanged(ControllerInfo) in its callback onSubscribe(), + // but with a different media ID. + // Therefore, onChildrenChanged() should not be called. + assertThat(latch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); + } + + private void setExpectedLibraryParam(MediaBrowser browser, LibraryParams params) + throws Exception { + SessionCommand command = new SessionCommand(CUSTOM_ACTION_ASSERT_PARAMS, null); + Bundle args = new Bundle(); + args.putBundle(CUSTOM_ACTION_ASSERT_PARAMS, params.toBundle()); + SessionResult result = browser.sendCustomCommand(command, args).get(TIMEOUT_MS, MILLISECONDS); + assertThat(result.resultCode).isEqualTo(SessionResult.RESULT_SUCCESS); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaBrowserCallbackWithMediaBrowserServiceCompatTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaBrowserCallbackWithMediaBrowserServiceCompatTest.java new file mode 100644 index 0000000000..ae33ff5d06 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaBrowserCallbackWithMediaBrowserServiceCompatTest.java @@ -0,0 +1,149 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.LibraryResult.RESULT_SUCCESS; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.MOCK_MEDIA_BROWSER_SERVICE_COMPAT; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserServiceCompatConstants.TEST_CONNECT_REJECTED; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserServiceCompatConstants.TEST_ON_CHILDREN_CHANGED_SUBSCRIBE_AND_UNSUBSCRIBE; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.TIMEOUT_MS; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.media.MediaBrowserServiceCompat; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import com.google.android.exoplayer2.session.MediaBrowser.BrowserCallback; +import com.google.android.exoplayer2.session.MediaLibraryService.LibraryParams; +import com.google.android.exoplayer2.session.vct.common.HandlerThreadTestRule; +import java.util.concurrent.CountDownLatch; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; + +/** Tests for {@link BrowserCallback} with {@link MediaBrowserServiceCompat}. */ +@RunWith(AndroidJUnit4.class) +@LargeTest +public class MediaBrowserCallbackWithMediaBrowserServiceCompatTest { + + private final HandlerThreadTestRule threadTestRule = + new HandlerThreadTestRule("MediaBrowserCallbackTestWithMediaBrowserServiceCompat"); + private final MediaControllerTestRule controllerTestRule = + new MediaControllerTestRule(threadTestRule); + + @Rule + public final TestRule chain = RuleChain.outerRule(threadTestRule).around(controllerTestRule); + + private Context context; + private RemoteMediaBrowserServiceCompat remoteService; + + private MediaBrowser createBrowser(boolean waitForConnect, @Nullable BrowserCallback callback) + throws Exception { + SessionToken token = new SessionToken(context, MOCK_MEDIA_BROWSER_SERVICE_COMPAT); + return (MediaBrowser) + controllerTestRule.createController( + token, waitForConnect, /* connectionHints= */ null, callback); + } + + @Before + public void setUp() { + controllerTestRule.setControllerType(MediaBrowser.class); + context = ApplicationProvider.getApplicationContext(); + remoteService = new RemoteMediaBrowserServiceCompat(context); + } + + @After + public void cleanUp() throws Exception { + remoteService.release(); + } + + @Test + public void connect() throws Exception { + createBrowser(/* waitForConnect= */ true, new BrowserCallback() {}); + // If connection failed, exception will be thrown inside of #createBrowser(). + } + + @Test + public void connect_rejected() throws Exception { + remoteService.setProxyForTest(TEST_CONNECT_REJECTED); + + CountDownLatch latch = new CountDownLatch(1); + createBrowser( + /* waitForConnect= */ false, + new BrowserCallback() { + @Override + public void onConnected(MediaController controller) { + assertWithMessage("shouldn't allow connection").fail(); + } + + @Override + public void onDisconnected(@NonNull MediaController controller) { + latch.countDown(); + } + }); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void onChildrenChanged_subscribeAndUnsubscribe() throws Exception { + String testParentId = "testOnChildrenChanged"; + CountDownLatch latch = new CountDownLatch(2); + BrowserCallback browserCallback = + new BrowserCallback() { + @Override + public void onChildrenChanged( + @NonNull MediaBrowser browser, + @NonNull String parentId, + int itemCount, + @Nullable LibraryParams params) { + // Triggered by both subscribe and notifyChildrenChanged(). + // Shouldn't be called after the unsubscribe(). + assertThat(latch.getCount()).isNotEqualTo(0); + assertThat(parentId).isEqualTo(testParentId); + assertThat(params).isNull(); + latch.countDown(); + } + }; + + remoteService.setProxyForTest(TEST_ON_CHILDREN_CHANGED_SUBSCRIBE_AND_UNSUBSCRIBE); + MediaBrowser browser = createBrowser(/* waitForConnect= */ true, browserCallback); + + LibraryResult resultForSubscribe = + browser.subscribe(testParentId, null).get(TIMEOUT_MS, MILLISECONDS); + assertThat(resultForSubscribe.resultCode).isEqualTo(RESULT_SUCCESS); + remoteService.notifyChildrenChanged(testParentId); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + + LibraryResult resultForUnsubscribe = + browser.unsubscribe(testParentId).get(TIMEOUT_MS, MILLISECONDS); + assertThat(resultForUnsubscribe.resultCode).isEqualTo(RESULT_SUCCESS); + // Unsubscribe takes some time. Wait for some time. + Thread.sleep(TIMEOUT_MS); + remoteService.notifyChildrenChanged(testParentId); + // This shouldn't trigger browser's onChildrenChanged(). + // Wait for some time. Exception will be thrown in the callback if error happens. + Thread.sleep(TIMEOUT_MS); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaBrowserCompatWithMediaLibraryServiceTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaBrowserCompatWithMediaLibraryServiceTest.java new file mode 100644 index 0000000000..13e40f7e43 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaBrowserCompatWithMediaLibraryServiceTest.java @@ -0,0 +1,463 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.MOCK_MEDIA2_LIBRARY_SERVICE; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.CHILDREN_COUNT; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.CUSTOM_ACTION; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.CUSTOM_ACTION_EXTRAS; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.GET_CHILDREN_RESULT; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.LONG_LIST_COUNT; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.MEDIA_ID_GET_ITEM; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.PARENT_ID; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.PARENT_ID_ERROR; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.PARENT_ID_LONG_LIST; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.PARENT_ID_NO_CHILDREN; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.ROOT_EXTRAS; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.ROOT_ID; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.SEARCH_QUERY; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.SEARCH_QUERY_EMPTY_RESULT; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.SEARCH_QUERY_ERROR; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.SEARCH_QUERY_LONG_LIST; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.SEARCH_RESULT; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.SEARCH_RESULT_COUNT; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.LONG_TIMEOUT_MS; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.NO_RESPONSE_TIMEOUT_MS; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.TIMEOUT_MS; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.ComponentName; +import android.os.Bundle; +import android.support.v4.media.MediaBrowserCompat; +import android.support.v4.media.MediaBrowserCompat.CustomActionCallback; +import android.support.v4.media.MediaBrowserCompat.ItemCallback; +import android.support.v4.media.MediaBrowserCompat.MediaItem; +import android.support.v4.media.MediaBrowserCompat.SearchCallback; +import android.support.v4.media.MediaBrowserCompat.SubscriptionCallback; +import androidx.annotation.NonNull; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import com.google.android.exoplayer2.session.vct.common.TestUtils; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link MediaBrowserCompat} with {@link MediaLibraryService}. */ +@RunWith(AndroidJUnit4.class) +@LargeTest +public class MediaBrowserCompatWithMediaLibraryServiceTest + extends MediaBrowserCompatWithMediaSessionServiceTest { + + @Override + ComponentName getServiceComponent() { + return MOCK_MEDIA2_LIBRARY_SERVICE; + } + + @Test + public void getRoot() throws Exception { + // The MockMediaLibraryService gives MediaBrowserConstants.ROOT_ID as root ID, and + // MediaBrowserConstants.ROOT_EXTRAS as extras. + handler.postAndSync( + () -> { + browserCompat = + new MediaBrowserCompat( + context, getServiceComponent(), connectionCallback, /* rootHint= */ null); + }); + connectAndWait(); + assertThat(browserCompat.getRoot()).isEqualTo(ROOT_ID); + + // Note: Cannot use equals() here because browser compat's extra contains server version, + // extra binder, and extra messenger. + assertThat(TestUtils.contains(browserCompat.getExtras(), ROOT_EXTRAS)).isTrue(); + } + + @Test + public void getItem() throws InterruptedException { + String mediaId = MEDIA_ID_GET_ITEM; + + connectAndWait(); + CountDownLatch latch = new CountDownLatch(1); + browserCompat.getItem( + mediaId, + new ItemCallback() { + @Override + public void onItemLoaded(MediaItem item) { + assertThat(item.getMediaId()).isEqualTo(mediaId); + assertThat(item).isNotNull(); + latch.countDown(); + } + }); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void getItem_nullResult() throws InterruptedException { + String mediaId = "random_media_id"; + + connectAndWait(); + CountDownLatch latch = new CountDownLatch(1); + browserCompat.getItem( + mediaId, + new ItemCallback() { + @Override + public void onItemLoaded(MediaItem item) { + assertThat(item).isNull(); + latch.countDown(); + } + + @Override + public void onError(@NonNull String itemId) { + assertWithMessage("").fail(); + } + }); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void getChildren() throws InterruptedException { + String testParentId = PARENT_ID; + + connectAndWait(); + CountDownLatch latch = new CountDownLatch(1); + browserCompat.subscribe( + testParentId, + new SubscriptionCallback() { + @Override + public void onChildrenLoaded( + @NonNull String parentId, @NonNull List children) { + assertThat(parentId).isEqualTo(testParentId); + assertThat(children).isNotNull(); + assertThat(children.size()).isEqualTo(GET_CHILDREN_RESULT.size()); + + // Compare the given results with originals. + for (int i = 0; i < children.size(); i++) { + assertThat(children.get(i).getMediaId()).isEqualTo(GET_CHILDREN_RESULT.get(i)); + } + latch.countDown(); + } + + @Override + public void onChildrenLoaded( + @NonNull String parentId, @NonNull List children, @NonNull Bundle option) { + assertWithMessage("").fail(); + } + }); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void getChildren_withLongList() throws InterruptedException { + String testParentId = PARENT_ID_LONG_LIST; + + connectAndWait(); + CountDownLatch latch = new CountDownLatch(1); + browserCompat.subscribe( + testParentId, + new SubscriptionCallback() { + @Override + public void onChildrenLoaded( + @NonNull String parentId, @NonNull List children) { + assertThat(parentId).isEqualTo(testParentId); + assertThat(children).isNotNull(); + assertThat(children.size() < LONG_LIST_COUNT).isTrue(); + + // Compare the given results with originals. + for (int i = 0; i < children.size(); i++) { + assertThat(children.get(i).getMediaId()) + .isEqualTo(TestUtils.getMediaIdInFakeTimeline(i)); + } + latch.countDown(); + } + + @Override + public void onChildrenLoaded( + @NonNull String parentId, @NonNull List children, @NonNull Bundle option) { + assertWithMessage("").fail(); + } + }); + assertThat(latch.await(LONG_TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void getChildren_withPagination() throws InterruptedException { + String testParentId = PARENT_ID; + int page = 4; + int pageSize = 10; + Bundle extras = new Bundle(); + extras.putString(testParentId, testParentId); + + connectAndWait(); + CountDownLatch latch = new CountDownLatch(1); + Bundle option = new Bundle(); + option.putInt(MediaBrowserCompat.EXTRA_PAGE, page); + option.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize); + browserCompat.subscribe( + testParentId, + option, + new SubscriptionCallback() { + @Override + public void onChildrenLoaded( + @NonNull String parentId, + @NonNull List children, + @NonNull Bundle options) { + assertThat(parentId).isEqualTo(testParentId); + assertThat(option.getInt(MediaBrowserCompat.EXTRA_PAGE)).isEqualTo(page); + assertThat(option.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE)).isEqualTo(pageSize); + assertThat(children).isNotNull(); + + int fromIndex = page * pageSize; + int toIndex = Math.min((page + 1) * pageSize, CHILDREN_COUNT); + + // Compare the given results with originals. + for (int originalIndex = fromIndex; originalIndex < toIndex; originalIndex++) { + int relativeIndex = originalIndex - fromIndex; + assertThat(children.get(relativeIndex).getMediaId()) + .isEqualTo(GET_CHILDREN_RESULT.get(originalIndex)); + } + latch.countDown(); + } + + @Override + public void onChildrenLoaded( + @NonNull String parentId, @NonNull List children) { + assertWithMessage("").fail(); + } + }); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void getChildren_emptyResult() throws InterruptedException { + String testParentId = PARENT_ID_NO_CHILDREN; + + connectAndWait(); + CountDownLatch latch = new CountDownLatch(1); + browserCompat.subscribe( + testParentId, + new SubscriptionCallback() { + @Override + public void onChildrenLoaded( + @NonNull String parentId, @NonNull List children) { + assertThat(children).isNotNull(); + assertThat(children.size()).isEqualTo(0); + latch.countDown(); + } + }); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void getChildren_nullResult() throws InterruptedException { + String testParentId = PARENT_ID_ERROR; + + connectAndWait(); + CountDownLatch latch = new CountDownLatch(1); + browserCompat.subscribe( + testParentId, + new SubscriptionCallback() { + @Override + public void onError(@NonNull String parentId) { + assertThat(parentId).isEqualTo(testParentId); + latch.countDown(); + } + + @Override + public void onChildrenLoaded( + @NonNull String parentId, + @NonNull List children, + @NonNull Bundle options) { + assertWithMessage("").fail(); + } + }); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void search() throws InterruptedException { + String testQuery = SEARCH_QUERY; + int page = 4; + int pageSize = 10; + Bundle testExtras = new Bundle(); + testExtras.putString(testQuery, testQuery); + testExtras.putInt(MediaBrowserCompat.EXTRA_PAGE, page); + testExtras.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize); + + connectAndWait(); + CountDownLatch latch = new CountDownLatch(1); + browserCompat.search( + testQuery, + testExtras, + new SearchCallback() { + @Override + public void onSearchResult( + @NonNull String query, Bundle extras, @NonNull List items) { + assertThat(query).isEqualTo(testQuery); + assertThat(TestUtils.equals(testExtras, extras)).isTrue(); + int expectedSize = + Math.max(Math.min(pageSize, SEARCH_RESULT_COUNT - pageSize * page), 0); + assertThat(items.size()).isEqualTo(expectedSize); + + int fromIndex = page * pageSize; + int toIndex = Math.min((page + 1) * pageSize, SEARCH_RESULT_COUNT); + + // Compare the given results with originals. + for (int originalIndex = fromIndex; originalIndex < toIndex; originalIndex++) { + int relativeIndex = originalIndex - fromIndex; + assertThat(items.get(relativeIndex).getMediaId()) + .isEqualTo(SEARCH_RESULT.get(originalIndex)); + } + latch.countDown(); + } + }); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void search_withLongList() throws InterruptedException { + String testQuery = SEARCH_QUERY_LONG_LIST; + int page = 0; + int pageSize = Integer.MAX_VALUE; + Bundle testExtras = new Bundle(); + testExtras.putString(testQuery, testQuery); + testExtras.putInt(MediaBrowserCompat.EXTRA_PAGE, page); + testExtras.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize); + + connectAndWait(); + CountDownLatch latch = new CountDownLatch(1); + browserCompat.search( + testQuery, + testExtras, + new SearchCallback() { + @Override + public void onSearchResult( + @NonNull String query, Bundle extras, @NonNull List items) { + assertThat(query).isEqualTo(testQuery); + assertThat(TestUtils.equals(testExtras, extras)).isTrue(); + + assertThat(items).isNotNull(); + assertThat(items.size() < LONG_LIST_COUNT).isTrue(); + for (int i = 0; i < items.size(); i++) { + assertThat(items.get(i).getMediaId()) + .isEqualTo(TestUtils.getMediaIdInFakeTimeline(i)); + } + latch.countDown(); + } + }); + assertThat(latch.await(LONG_TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void search_emptyResult() throws InterruptedException { + String testQuery = SEARCH_QUERY_EMPTY_RESULT; + Bundle testExtras = new Bundle(); + testExtras.putString(testQuery, testQuery); + + connectAndWait(); + CountDownLatch latch = new CountDownLatch(1); + browserCompat.search( + testQuery, + testExtras, + new SearchCallback() { + @Override + public void onSearchResult( + @NonNull String query, Bundle extras, @NonNull List items) { + assertThat(query).isEqualTo(testQuery); + assertThat(TestUtils.equals(testExtras, extras)).isTrue(); + assertThat(items).isNotNull(); + assertThat(items.size()).isEqualTo(0); + latch.countDown(); + } + }); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void search_error() throws InterruptedException { + String testQuery = SEARCH_QUERY_ERROR; + Bundle testExtras = new Bundle(); + testExtras.putString(testQuery, testQuery); + + connectAndWait(); + CountDownLatch latch = new CountDownLatch(1); + browserCompat.search( + testQuery, + testExtras, + new SearchCallback() { + @Override + public void onError(@NonNull String query, Bundle extras) { + assertThat(query).isEqualTo(testQuery); + assertThat(TestUtils.equals(testExtras, extras)).isTrue(); + latch.countDown(); + } + + @Override + public void onSearchResult( + @NonNull String query, Bundle extras, @NonNull List items) { + assertWithMessage("").fail(); + } + }); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + // TODO: Add test for onCustomCommand() in MediaLibrarySessionLegacyCallbackTest. + @Test + public void customAction() throws InterruptedException { + Bundle testArgs = new Bundle(); + testArgs.putString("args_key", "args_value"); + + connectAndWait(); + CountDownLatch latch = new CountDownLatch(1); + browserCompat.sendCustomAction( + CUSTOM_ACTION, + testArgs, + new CustomActionCallback() { + @Override + public void onResult(String action, Bundle extras, Bundle resultData) { + assertThat(action).isEqualTo(CUSTOM_ACTION); + assertThat(TestUtils.equals(testArgs, extras)).isTrue(); + assertThat(TestUtils.equals(CUSTOM_ACTION_EXTRAS, resultData)).isTrue(); + latch.countDown(); + } + }); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + // TODO: Add test for onCustomCommand() in MediaLibrarySessionLegacyCallbackTest. + @Test + public void customAction_rejected() throws InterruptedException { + // This action will not be allowed by the library session. + String testAction = "random_custom_action"; + + connectAndWait(); + CountDownLatch latch = new CountDownLatch(1); + browserCompat.sendCustomAction( + testAction, + null, + new CustomActionCallback() { + @Override + public void onResult(String action, Bundle extras, Bundle resultData) { + latch.countDown(); + } + }); + assertWithMessage("BrowserCompat shouldn't receive custom command") + .that(latch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)) + .isFalse(); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaBrowserCompatWithMediaSessionServiceTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaBrowserCompatWithMediaSessionServiceTest.java new file mode 100644 index 0000000000..b2c6f56314 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaBrowserCompatWithMediaSessionServiceTest.java @@ -0,0 +1,143 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.MOCK_MEDIA2_SESSION_SERVICE; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.SERVICE_CONNECTION_TIMEOUT_MS; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.TIMEOUT_MS; +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.ComponentName; +import android.content.Context; +import android.support.v4.media.MediaBrowserCompat; +import android.support.v4.media.session.MediaControllerCompat; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import com.google.android.exoplayer2.session.vct.common.HandlerThreadTestRule; +import com.google.android.exoplayer2.session.vct.common.TestHandler; +import java.util.concurrent.CountDownLatch; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; + +/** Tests for {@link MediaBrowserCompat} with {@link MediaSessionService}. */ +@RunWith(AndroidJUnit4.class) +@LargeTest +public class MediaBrowserCompatWithMediaSessionServiceTest { + + private final HandlerThreadTestRule threadTestRule = + new HandlerThreadTestRule("MediaBrowserCompatTestWithMediaSessionService"); + private final MediaControllerTestRule controllerTestRule = + new MediaControllerTestRule(threadTestRule); + + @Rule + public final TestRule chain = RuleChain.outerRule(threadTestRule).around(controllerTestRule); + + Context context; + TestHandler handler; + MediaBrowserCompat browserCompat; + TestConnectionCallback connectionCallback; + + @Before + public void setUp() throws Exception { + context = ApplicationProvider.getApplicationContext(); + handler = threadTestRule.getHandler(); + connectionCallback = new TestConnectionCallback(); + handler.postAndSync( + () -> { + // Make browser's internal handler to be initialized with test thread. + browserCompat = + new MediaBrowserCompat(context, getServiceComponent(), connectionCallback, null); + }); + } + + @After + public void cleanUp() { + if (browserCompat != null) { + browserCompat.disconnect(); + browserCompat = null; + } + } + + ComponentName getServiceComponent() { + return MOCK_MEDIA2_SESSION_SERVICE; + } + + void connectAndWait() throws InterruptedException { + browserCompat.connect(); + assertThat(connectionCallback.connectedLatch.await(SERVICE_CONNECTION_TIMEOUT_MS, MILLISECONDS)) + .isTrue(); + } + + @Test + public void connect() throws InterruptedException { + connectAndWait(); + assertThat(connectionCallback.failedLatch.getCount()).isNotEqualTo(0); + } + + @Ignore + @Test + public void connect_rejected() throws InterruptedException { + // TODO: Connect the browser to the session service whose onConnect() returns null. + assertThat(connectionCallback.failedLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(connectionCallback.connectedLatch.getCount()).isNotEqualTo(0); + } + + @Test + public void getSessionToken() throws Exception { + connectAndWait(); + MediaControllerCompat controller = + new MediaControllerCompat(context, browserCompat.getSessionToken()); + assertThat(controller.getPackageName()) + .isEqualTo(browserCompat.getServiceComponent().getPackageName()); + } + + class TestConnectionCallback extends MediaBrowserCompat.ConnectionCallback { + public final CountDownLatch connectedLatch = new CountDownLatch(1); + public final CountDownLatch suspendedLatch = new CountDownLatch(1); + public final CountDownLatch failedLatch = new CountDownLatch(1); + + TestConnectionCallback() { + super(); + } + + @Override + public void onConnected() { + super.onConnected(); + connectedLatch.countDown(); + } + + @Override + public void onConnectionSuspended() { + super.onConnectionSuspended(); + suspendedLatch.countDown(); + } + + @Override + public void onConnectionFailed() { + super.onConnectionFailed(); + failedLatch.countDown(); + } + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaBrowserServiceCompatCallbackWithMediaBrowserTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaBrowserServiceCompatCallbackWithMediaBrowserTest.java new file mode 100644 index 0000000000..07b14583c8 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaBrowserServiceCompatCallbackWithMediaBrowserTest.java @@ -0,0 +1,453 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.TIMEOUT_MS; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.ComponentName; +import android.content.Context; +import android.os.Bundle; +import android.support.v4.media.MediaBrowserCompat; +import android.support.v4.media.MediaDescriptionCompat; +import androidx.media.MediaBrowserServiceCompat; +import androidx.media.MediaBrowserServiceCompat.BrowserRoot; +import androidx.media.MediaBrowserServiceCompat.Result; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.session.MediaLibraryService.LibraryParams; +import com.google.android.exoplayer2.session.MockMediaBrowserServiceCompat.Proxy; +import com.google.android.exoplayer2.session.vct.common.MainLooperTestRule; +import com.google.android.exoplayer2.session.vct.common.TestUtils; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link MediaBrowserServiceCompat} with {@link MediaBrowser}. */ +@RunWith(AndroidJUnit4.class) +@LargeTest +public class MediaBrowserServiceCompatCallbackWithMediaBrowserTest { + + @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); + + private Context context; + private SessionToken token; + + @Before + public void setUp() { + context = ApplicationProvider.getApplicationContext(); + token = + new SessionToken( + context, new ComponentName(context, LocalMockMediaBrowserServiceCompat.class)); + } + + @Test + public void onGetRootCalledByGetLibraryRoot() throws Exception { + String testMediaId = "testOnGetRootCalledByGetLibraryRoot"; + Bundle testExtras = new Bundle(); + testExtras.putString(testMediaId, testMediaId); + LibraryParams testParams = + new LibraryParams.Builder().setSuggested(true).setExtras(testExtras).build(); + + Bundle testReturnedExtras = new Bundle(testExtras); + testReturnedExtras.putBoolean(BrowserRoot.EXTRA_OFFLINE, true); + BrowserRoot browserRoot = new BrowserRoot(testMediaId, testReturnedExtras); + + CountDownLatch latch = new CountDownLatch(1); + MockMediaBrowserServiceCompat.setMediaBrowserServiceProxy( + new Proxy() { + @Override + public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { + assertThat(clientPackageName).isEqualTo(SUPPORT_APP_PACKAGE_NAME); + if (rootHints != null && rootHints.keySet().contains(testMediaId)) { + MediaTestUtils.assertLibraryParamsEquals(testParams, rootHints); + // This should happen because getLibraryRoot() is called with testExtras. + latch.countDown(); + return browserRoot; + } + // For other random connection requests. + return new BrowserRoot("rootId", null); + } + }); + + RemoteMediaBrowser browser = new RemoteMediaBrowser(context, token, true, null); + LibraryResult result = browser.getLibraryRoot(testParams); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + + assertThat(result.resultCode).isEqualTo(LibraryResult.RESULT_SUCCESS); + assertThat(result.item.mediaId).isEqualTo(testMediaId); + + LibraryParams returnedParams = result.params; + assertThat(returnedParams.recent) + .isEqualTo(testReturnedExtras.getBoolean(BrowserRoot.EXTRA_RECENT)); + assertThat(returnedParams.offline) + .isEqualTo(testReturnedExtras.getBoolean(BrowserRoot.EXTRA_OFFLINE)); + assertThat(returnedParams.suggested) + .isEqualTo(testReturnedExtras.getBoolean(BrowserRoot.EXTRA_SUGGESTED)); + + // Note that TestUtils#equals() cannot be used for this because + // MediaBrowserServiceCompat adds extra_client_version to the rootHints. + assertThat(TestUtils.contains(returnedParams.extras, testExtras)).isTrue(); + } + + @Test + public void onLoadItemCalledByGetItem() throws Exception { + String testMediaId = "test_media_item"; + MediaBrowserCompat.MediaItem testItem = createBrowserMediaItem(testMediaId); + CountDownLatch latch = new CountDownLatch(1); + MockMediaBrowserServiceCompat.setMediaBrowserServiceProxy( + new Proxy() { + @Override + public void onLoadItem(String itemId, Result result) { + assertThat(itemId).isEqualTo(testMediaId); + result.sendResult(testItem); + latch.countDown(); + } + }); + + RemoteMediaBrowser browser = new RemoteMediaBrowser(context, token, true, null); + LibraryResult result = browser.getItem(testMediaId); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(result.resultCode).isEqualTo(LibraryResult.RESULT_SUCCESS); + assertItemEquals(testItem, result.item); + } + + @Test + public void onLoadItemCalledByGetItem_nullResult() throws Exception { + String testMediaId = "test_media_item"; + CountDownLatch latch = new CountDownLatch(1); + MockMediaBrowserServiceCompat.setMediaBrowserServiceProxy( + new Proxy() { + @Override + public void onLoadItem(String itemId, Result result) { + assertThat(itemId).isEqualTo(testMediaId); + result.sendResult(null); + latch.countDown(); + } + }); + + RemoteMediaBrowser browser = new RemoteMediaBrowser(context, token, true, null); + LibraryResult result = browser.getItem(testMediaId); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(result.resultCode).isNotEqualTo(LibraryResult.RESULT_SUCCESS); + } + + @Test + public void onLoadChildrenWithoutOptionsCalledByGetChildrenWithoutOptions() throws Exception { + String testParentId = "test_media_parent"; + int testPage = 2; + int testPageSize = 4; + List testFullMediaItemList = + createBrowserMediaItems((testPage + 1) * testPageSize); + List testPaginatedMediaItemList = + testFullMediaItemList.subList( + testPage * testPageSize, + Math.min((testPage + 1) * testPageSize, testFullMediaItemList.size())); + CountDownLatch latch = new CountDownLatch(1); + MockMediaBrowserServiceCompat.setMediaBrowserServiceProxy( + new Proxy() { + @Override + public void onLoadChildren( + String parentId, Result> result) { + assertThat(parentId).isEqualTo(testParentId); + result.sendResult(testFullMediaItemList); + latch.countDown(); + } + }); + RemoteMediaBrowser browser = new RemoteMediaBrowser(context, token, true, null); + LibraryResult result = browser.getChildren(testParentId, testPage, testPageSize, null); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(result.resultCode).isEqualTo(LibraryResult.RESULT_SUCCESS); + assertItemsEquals(testPaginatedMediaItemList, result.items); + } + + @Test + public void onLoadChildrenWithOptionsCalledByGetChildrenWithoutOptions() throws Exception { + String testParentId = "test_media_parent"; + int testPage = 2; + int testPageSize = 4; + List testMediaItemList = createBrowserMediaItems(testPageSize); + CountDownLatch latch = new CountDownLatch(1); + MockMediaBrowserServiceCompat.setMediaBrowserServiceProxy( + new Proxy() { + @Override + public void onLoadChildren( + String parentId, Result> result) { + assertWithMessage("This isn't expected to be called").fail(); + } + + @Override + public void onLoadChildren( + String parentId, Result> result, Bundle options) { + assertThat(parentId).isEqualTo(testParentId); + assertThat(options.getInt(MediaBrowserCompat.EXTRA_PAGE)).isEqualTo(testPage); + assertThat(options.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE)).isEqualTo(testPageSize); + assertThat(options.keySet().size()).isEqualTo(2); + result.sendResult(testMediaItemList); + latch.countDown(); + } + }); + RemoteMediaBrowser browser = new RemoteMediaBrowser(context, token, true, null); + LibraryResult result = browser.getChildren(testParentId, testPage, testPageSize, null); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(result.resultCode).isEqualTo(LibraryResult.RESULT_SUCCESS); + assertItemsEquals(testMediaItemList, result.items); + assertThat(result.params).isNull(); + } + + @Test + public void onLoadChildrenWithOptionsCalledByGetChildrenWithoutOptions_nullResult() + throws Exception { + String testParentId = "test_media_parent"; + int testPage = 2; + int testPageSize = 4; + CountDownLatch latch = new CountDownLatch(1); + MockMediaBrowserServiceCompat.setMediaBrowserServiceProxy( + new Proxy() { + @Override + public void onLoadChildren( + String parentId, Result> result, Bundle options) { + assertThat(parentId).isEqualTo(testParentId); + assertThat(options.getInt(MediaBrowserCompat.EXTRA_PAGE)).isEqualTo(testPage); + assertThat(options.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE)).isEqualTo(testPageSize); + result.sendResult(null); + latch.countDown(); + } + }); + RemoteMediaBrowser browser = new RemoteMediaBrowser(context, token, true, null); + LibraryResult result = browser.getChildren(testParentId, testPage, testPageSize, null); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(result.resultCode).isNotEqualTo(LibraryResult.RESULT_SUCCESS); + assertThat(result.params).isNull(); + } + + @Test + public void onLoadChildrenWithOptionsCalledByGetChildrenWithOptions() throws Exception { + String testParentId = "test_media_parent"; + int testPage = 2; + int testPageSize = 4; + LibraryParams testParams = MediaTestUtils.createLibraryParams(); + List testMediaItemList = + createBrowserMediaItems(testPageSize / 2); + CountDownLatch latch = new CountDownLatch(1); + MockMediaBrowserServiceCompat.setMediaBrowserServiceProxy( + new Proxy() { + @Override + public void onLoadChildren( + String parentId, Result> result, Bundle options) { + assertThat(parentId).isEqualTo(testParentId); + assertThat(options.getInt(MediaBrowserCompat.EXTRA_PAGE)).isEqualTo(testPage); + assertThat(options.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE)).isEqualTo(testPageSize); + assertThat(TestUtils.contains(options, testParams.extras)).isTrue(); + result.sendResult(testMediaItemList); + latch.countDown(); + } + }); + RemoteMediaBrowser browser = new RemoteMediaBrowser(context, token, true, null); + LibraryResult result = browser.getChildren(testParentId, testPage, testPageSize, testParams); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(result.resultCode).isEqualTo(LibraryResult.RESULT_SUCCESS); + assertItemsEquals(testMediaItemList, result.items); + assertThat(result.params).isNull(); + } + + @Test + public void onLoadChildrenCalledBySubscribe() throws Exception { + String testParentId = "testOnLoadChildrenCalledBySubscribe"; + LibraryParams testParams = MediaTestUtils.createLibraryParams(); + CountDownLatch subscribeLatch = new CountDownLatch(1); + MockMediaBrowserServiceCompat.setMediaBrowserServiceProxy( + new Proxy() { + @Override + public void onLoadChildren( + String parentId, Result> result, Bundle option) { + assertThat(parentId).isEqualTo(testParentId); + MediaTestUtils.assertLibraryParamsEquals(testParams, option); + result.sendResult(Collections.emptyList()); + subscribeLatch.countDown(); + } + }); + RemoteMediaBrowser browser = new RemoteMediaBrowser(context, token, true, null); + LibraryResult result = browser.subscribe(testParentId, testParams); + assertThat(subscribeLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(result.resultCode).isEqualTo(LibraryResult.RESULT_SUCCESS); + } + + @Test + public void onLoadChildrenCalledBySubscribe_failed() throws Exception { + String testParentId = "testOnLoadChildrenCalledBySubscribe_failed"; + CountDownLatch subscribeLatch = new CountDownLatch(1); + MockMediaBrowserServiceCompat.setMediaBrowserServiceProxy( + new Proxy() { + @Override + public void onLoadChildren( + String parentId, Result> result, Bundle option) { + assertThat(parentId).isEqualTo(testParentId); + // Cannot use Result#sendError() for sending error here. The API is specific to + // custom action. + result.sendResult(null); + subscribeLatch.countDown(); + } + }); + RemoteMediaBrowser browser = new RemoteMediaBrowser(context, token, true, null); + LibraryResult result = browser.subscribe(testParentId, null); + assertThat(subscribeLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(result.resultCode).isNotEqualTo(LibraryResult.RESULT_SUCCESS); + } + + @Test + public void onSearchCalledBySearch() throws Exception { + String testQuery = "search_query"; + int testPage = 2; + int testPageSize = 4; + LibraryParams testParams = MediaTestUtils.createLibraryParams(); + List testFullSearchResult = + createBrowserMediaItems((testPage + 1) * testPageSize + 3); + + CountDownLatch latch = new CountDownLatch(1); + MockMediaBrowserServiceCompat.setMediaBrowserServiceProxy( + new Proxy() { + @Override + public void onSearch( + String query, Bundle extras, Result> result) { + assertThat(query).isEqualTo(testQuery); + MediaTestUtils.assertLibraryParamsEquals(testParams, extras); + result.sendResult(testFullSearchResult); + latch.countDown(); + } + }); + + RemoteMediaBrowser browser = new RemoteMediaBrowser(context, token, true, null); + LibraryResult result = browser.search(testQuery, testParams); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(result.resultCode).isEqualTo(LibraryResult.RESULT_SUCCESS); + } + + @Test + public void onSearchCalledBySearch_nullResult() throws Exception { + String testQuery = "search_query"; + + CountDownLatch latch = new CountDownLatch(1); + MockMediaBrowserServiceCompat.setMediaBrowserServiceProxy( + new Proxy() { + @Override + public void onSearch( + String query, Bundle extras, Result> result) { + result.sendResult(null); + latch.countDown(); + } + }); + + RemoteMediaBrowser browser = new RemoteMediaBrowser(context, token, true, null); + LibraryResult result = browser.search(testQuery, null); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(result.resultCode).isEqualTo(LibraryResult.RESULT_SUCCESS); + } + + @Test + public void onSearchCalledByGetSearchResult() throws Exception { + String testQuery = "search_query"; + int testPage = 2; + int testPageSize = 4; + LibraryParams testParams = MediaTestUtils.createLibraryParams(); + + CountDownLatch latch = new CountDownLatch(1); + MockMediaBrowserServiceCompat.setMediaBrowserServiceProxy( + new Proxy() { + @Override + public void onSearch( + String query, Bundle extras, Result> result) { + assertThat(query).isEqualTo(testQuery); + MediaTestUtils.assertLibraryParamsEquals(testParams, extras); + assertThat(extras.getInt(MediaBrowserCompat.EXTRA_PAGE)).isEqualTo(testPage); + assertThat(extras.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE)).isEqualTo(testPageSize); + result.sendResult(Collections.emptyList()); + latch.countDown(); + } + }); + + RemoteMediaBrowser browser = new RemoteMediaBrowser(context, token, true, null); + LibraryResult result = browser.getSearchResult(testQuery, testPage, testPageSize, testParams); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(result.resultCode).isEqualTo(LibraryResult.RESULT_SUCCESS); + } + + @Test + public void onSearchCalledByGetSearchResult_nullResult() throws Exception { + String testQuery = "search_query"; + int testPage = 2; + int testPageSize = 4; + LibraryParams testParams = MediaTestUtils.createLibraryParams(); + + CountDownLatch latch = new CountDownLatch(1); + MockMediaBrowserServiceCompat.setMediaBrowserServiceProxy( + new Proxy() { + @Override + public void onSearch( + String query, Bundle extras, Result> result) { + result.sendResult(null); + latch.countDown(); + } + }); + + RemoteMediaBrowser browser = new RemoteMediaBrowser(context, token, true, null); + LibraryResult result = browser.getSearchResult(testQuery, testPage, testPageSize, testParams); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(result.resultCode).isNotEqualTo(LibraryResult.RESULT_SUCCESS); + } + + private static MediaBrowserCompat.MediaItem createBrowserMediaItem(String mediaId) { + MediaDescriptionCompat desc = + new MediaDescriptionCompat.Builder() + .setMediaId(mediaId) + .setTitle("title: " + mediaId) + .build(); + return new MediaBrowserCompat.MediaItem(desc, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE); + } + + private static List createBrowserMediaItems(int size) { + List list = new ArrayList<>(); + for (int i = 0; i < size; i++) { + list.add(createBrowserMediaItem("browserItem_" + i)); + } + return list; + } + + private static void assertItemEquals( + MediaBrowserCompat.MediaItem browserItem, MediaItem commonItem) { + assertThat(commonItem.mediaId).isEqualTo(browserItem.getMediaId()); + assertThat(commonItem.mediaMetadata.title).isEqualTo(browserItem.getDescription().getTitle()); + } + + private static void assertItemsEquals( + List browserItemList, List commonItemList) { + assertThat(commonItemList.size()).isEqualTo(browserItemList.size()); + for (int i = 0; i < browserItemList.size(); i++) { + assertItemEquals(browserItemList.get(i), commonItemList.get(i)); + } + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaBrowserTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaBrowserTest.java new file mode 100644 index 0000000000..5f9a4ad8cf --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaBrowserTest.java @@ -0,0 +1,33 @@ +/* + * 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.google.android.exoplayer2.session; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import org.junit.Before; +import org.junit.runner.RunWith; + +/** Tests for {@link MediaBrowser}. */ +@RunWith(AndroidJUnit4.class) +@LargeTest +public class MediaBrowserTest extends MediaControllerTest { + + @Before + public void setControllerType() { + controllerTestRule.setControllerType(MediaBrowser.class); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaControllerCallbackTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaControllerCallbackTest.java new file mode 100644 index 0000000000..374e74763d --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaControllerCallbackTest.java @@ -0,0 +1,1909 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.Player.COMMAND_PLAY_PAUSE; +import static com.google.android.exoplayer2.Player.COMMAND_SET_REPEAT_MODE; +import static com.google.android.exoplayer2.Player.EVENT_REPEAT_MODE_CHANGED; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; +import static com.google.android.exoplayer2.Player.STATE_BUFFERING; +import static com.google.android.exoplayer2.session.MediaUtils.createPlayerCommandsWith; +import static com.google.android.exoplayer2.session.MediaUtils.createPlayerCommandsWithAllCommands; +import static com.google.android.exoplayer2.session.SessionResult.RESULT_SUCCESS; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.DEFAULT_TEST_NAME; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.MOCK_MEDIA2_LIBRARY_SERVICE; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.MOCK_MEDIA2_SESSION_SERVICE; +import static com.google.android.exoplayer2.session.vct.common.MediaSessionConstants.TEST_CONTROLLER_CALLBACK_SESSION_REJECTS; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.LONG_TIMEOUT_MS; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.NO_RESPONSE_TIMEOUT_MS; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.TIMEOUT_MS; +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.Context; +import android.media.AudioManager; +import android.os.Bundle; +import android.os.RemoteException; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.media.AudioAttributesCompat; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaMetadata; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.Commands; +import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.PlayWhenReadyChangeReason; +import com.google.android.exoplayer2.Player.PositionInfo; +import com.google.android.exoplayer2.Player.RepeatMode; +import com.google.android.exoplayer2.Player.State; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.audio.AudioAttributes; +import com.google.android.exoplayer2.device.DeviceInfo; +import com.google.android.exoplayer2.session.RemoteMediaSession.RemoteMockPlayer; +import com.google.android.exoplayer2.session.SessionPlayer.PlayerCallback; +import com.google.android.exoplayer2.session.vct.common.HandlerThreadTestRule; +import com.google.android.exoplayer2.session.vct.common.MainLooperTestRule; +import com.google.android.exoplayer2.session.vct.common.TestUtils; +import com.google.android.exoplayer2.util.ExoFlags; +import com.google.android.exoplayer2.video.VideoSize; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; + +/** + * Tests for {@link MediaController.ControllerCallback}. It also tests {@link + * SessionPlayer.PlayerCallback} passed by {@link MediaController#addListener}. + */ +@RunWith(AndroidJUnit4.class) +@LargeTest +public class MediaControllerCallbackTest { + + @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); + + private static final int EVENT_ON_EVENTS = C.INDEX_UNSET; + + private final HandlerThreadTestRule threadTestRule = + new HandlerThreadTestRule("MediaControllerCallbackTest"); + final MediaControllerTestRule controllerTestRule = new MediaControllerTestRule(threadTestRule); + + @Rule + public final TestRule chain = RuleChain.outerRule(threadTestRule).around(controllerTestRule); + + Context context; + RemoteMediaSession remoteSession; + private MediaController controller; + + @Before + public void setUp() throws Exception { + context = ApplicationProvider.getApplicationContext(); + remoteSession = createRemoteMediaSession(DEFAULT_TEST_NAME); + } + + @After + public void cleanUp() throws RemoteException { + if (remoteSession != null) { + remoteSession.cleanUp(); + remoteSession = null; + } + } + + @Test + public void connection_sessionAccepts() throws Exception { + // createController() uses controller callback to wait until the controller becomes + // available. + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + assertThat(controller).isNotNull(); + } + + @Test + public void connection_sessionRejects() throws Exception { + RemoteMediaSession session = createRemoteMediaSession(TEST_CONTROLLER_CALLBACK_SESSION_REJECTS); + try { + MediaController controller = + controllerTestRule.createController( + session.getToken(), /* waitForConnect= */ false, null, null); + assertThat(controller).isNotNull(); + controllerTestRule.waitForConnect(controller, /* expected= */ false); + controllerTestRule.waitForDisconnect(controller, /* expected= */ true); + } finally { + session.cleanUp(); + } + } + + @Test + public void connection_toSessionService() throws Exception { + SessionToken token = new SessionToken(context, MOCK_MEDIA2_SESSION_SERVICE); + MediaController controller = controllerTestRule.createController(token); + assertThat(controller).isNotNull(); + } + + @Test + public void connection_toLibraryService() throws Exception { + SessionToken token = new SessionToken(context, MOCK_MEDIA2_LIBRARY_SERVICE); + MediaController controller = controllerTestRule.createController(token); + assertThat(controller).isNotNull(); + } + + @Test + public void connection_sessionClosed() throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + + remoteSession.release(); + controllerTestRule.waitForDisconnect(controller, true); + } + + @Test + public void connection_controllerClosed() throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + + threadTestRule.getHandler().postAndSync(controller::release); + controllerTestRule.waitForDisconnect(controller, true); + } + + @Test + @LargeTest + public void noInteractionAfterSessionClose_session() throws Exception { + SessionToken token = remoteSession.getToken(); + controller = controllerTestRule.createController(token); + testControllerAfterSessionIsClosed(DEFAULT_TEST_NAME); + } + + @Test + @LargeTest + public void noInteractionAfterControllerClose_session() throws Exception { + SessionToken token = remoteSession.getToken(); + controller = controllerTestRule.createController(token); + + threadTestRule.getHandler().postAndSync(controller::release); + // release is done immediately for session. + testNoInteraction(); + + // Test whether the controller is notified about later release of the session or + // re-creation. + testControllerAfterSessionIsClosed(DEFAULT_TEST_NAME); + } + + @Test + @LargeTest + public void connection_withLongPlaylist() throws Exception { + int windowCount = 5_000; + remoteSession.getMockPlayer().createAndSetFakeTimeline(windowCount); + + CountDownLatch latch = new CountDownLatch(1); + MediaController controller = + new MediaController.Builder(context) + .setSessionToken(remoteSession.getToken()) + .setControllerCallback( + new MediaController.ControllerCallback() { + @Override + public void onConnected(MediaController controller) { + latch.countDown(); + } + }) + .setApplicationLooper(threadTestRule.getHandler().getLooper()) + .build(); + + assertThat(latch.await(LONG_TIMEOUT_MS, MILLISECONDS)).isTrue(); + Timeline timeline = threadTestRule.getHandler().postAndSync(controller::getCurrentTimeline); + assertThat(timeline.getWindowCount()).isEqualTo(windowCount); + Timeline.Window window = new Timeline.Window(); + for (int i = 0; i < timeline.getWindowCount(); i++) { + assertThat(timeline.getWindow(i, window).mediaItem.mediaId) + .isEqualTo(TestUtils.getMediaIdInFakeTimeline(i)); + } + } + + @Test + public void onPlayerError_isNotified() throws Exception { + ExoPlaybackException testPlayerError = ExoPlaybackException.createForRemote("test exception"); + + AtomicReference playerErrorRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + controller.addListener( + new PlayerCallback() { + @Override + public void onPlayerError(ExoPlaybackException playerError) { + playerErrorRef.set(playerError); + latch.countDown(); + } + }); + + remoteSession.getMockPlayer().notifyPlayerError(testPlayerError); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(TestUtils.equals(playerErrorRef.get(), testPlayerError)).isTrue(); + } + + @Test + public void setPlayer_notifiesChangedValues() throws Exception { + @State int testState = STATE_BUFFERING; + Timeline testTimeline = MediaTestUtils.createTimeline(/* windowCount= */ 3); + MediaMetadata testPlaylistMetadata = new MediaMetadata.Builder().setTitle("title").build(); + AudioAttributes testAudioAttributes = + MediaUtils.convertToAudioAttributes( + new AudioAttributesCompat.Builder() + .setLegacyStreamType(AudioManager.STREAM_RING) + .build()); + boolean testShuffleModeEnabled = true; + @RepeatMode int testRepeatMode = REPEAT_MODE_ALL; + int testCurrentAdGroupIndex = 33; + int testCurrentAdIndexInAdGroup = 11; + + AtomicInteger stateRef = new AtomicInteger(); + AtomicReference timelineRef = new AtomicReference<>(); + AtomicReference playlistMetadataRef = new AtomicReference<>(); + AtomicReference audioAttributesRef = new AtomicReference<>(); + AtomicInteger currentAdGroupIndexRef = new AtomicInteger(); + AtomicInteger currentAdIndexInAdGroupRef = new AtomicInteger(); + AtomicBoolean shuffleModeEnabledRef = new AtomicBoolean(); + AtomicInteger repeatModeRef = new AtomicInteger(); + CountDownLatch latch = new CountDownLatch(7); + controller = controllerTestRule.createController(remoteSession.getToken()); + controller.addListener( + new SessionPlayer.PlayerCallback() { + @Override + public void onAudioAttributesChanged(@NonNull AudioAttributes attributes) { + audioAttributesRef.set(attributes); + latch.countDown(); + } + + @Override + public void onPlaybackStateChanged(@State int state) { + stateRef.set(state); + latch.countDown(); + } + + @Override + public void onTimelineChanged( + Timeline timeline, @Player.TimelineChangeReason int reason) { + timelineRef.set(timeline); + latch.countDown(); + } + + @Override + public void onPlaylistMetadataChanged(MediaMetadata playlistMetadata) { + playlistMetadataRef.set(playlistMetadata); + latch.countDown(); + } + + @Override + public void onPositionDiscontinuity( + PositionInfo oldPosition, PositionInfo newPosition, @DiscontinuityReason int reason) { + currentAdGroupIndexRef.set(newPosition.adGroupIndex); + currentAdIndexInAdGroupRef.set(newPosition.adIndexInAdGroup); + latch.countDown(); + } + + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + shuffleModeEnabledRef.set(shuffleModeEnabled); + latch.countDown(); + } + + @Override + public void onRepeatModeChanged(@RepeatMode int repeatMode) { + repeatModeRef.set(repeatMode); + latch.countDown(); + } + }); + + // TODO(b/149713425): Stop setting MediaItem as current media item. + // Currently it's necessary for trigger position discontinuity. + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setPlaybackState(testState) + .setAudioAttributes(testAudioAttributes) + .setTimeline(testTimeline) + .setPlaylistMetadata(testPlaylistMetadata) + .setCurrentMediaItem(MediaTestUtils.createConvergedMediaItem("mediaId")) + .setShuffleModeEnabled(testShuffleModeEnabled) + .setRepeatMode(testRepeatMode) + .setCurrentAdGroupIndex(testCurrentAdGroupIndex) + .setCurrentAdIndexInAdGroup(testCurrentAdIndexInAdGroup) + .build(); + + remoteSession.setPlayer(playerConfig); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(stateRef.get()).isEqualTo(testState); + MediaTestUtils.assertMediaIdEquals(testTimeline, timelineRef.get()); + assertThat(playlistMetadataRef.get()).isEqualTo(testPlaylistMetadata); + assertThat(audioAttributesRef.get()).isEqualTo(testAudioAttributes); + assertThat(currentAdGroupIndexRef.get()).isEqualTo(testCurrentAdGroupIndex); + assertThat(currentAdIndexInAdGroupRef.get()).isEqualTo(testCurrentAdIndexInAdGroup); + assertThat(shuffleModeEnabledRef.get()).isEqualTo(testShuffleModeEnabled); + assertThat(repeatModeRef.get()).isEqualTo(testRepeatMode); + } + + @Test + public void setPlayer_updatesGetters() throws Exception { + long testCurrentPositionMs = 11; + long testContentPositionMs = 33; + long testDurationMs = 200; + long testBufferedPositionMs = 100; + int testBufferedPercentage = 50; + long testTotalBufferedDurationMs = 120; + long testCurrentLiveOffsetMs = 10; + long testContentDurationMs = 300; + long testContentBufferedPositionMs = 240; + boolean testIsPlayingAd = true; + int testCurrentAdGroupIndex = 2; + int testCurrentAdIndexInAdGroup = 6; + int testWindowIndex = 1; + int testPeriodIndex = 2; + + controller = controllerTestRule.createController(remoteSession.getToken()); + + CountDownLatch latch = new CountDownLatch(1); + AtomicLong currentPositionMsRef = new AtomicLong(); + AtomicLong contentPositionMsRef = new AtomicLong(); + AtomicLong durationMsRef = new AtomicLong(); + AtomicLong bufferedPositionMsRef = new AtomicLong(); + AtomicInteger bufferedPercentageRef = new AtomicInteger(); + AtomicLong totalBufferedDurationMsRef = new AtomicLong(); + AtomicLong currentLiveOffsetMsRef = new AtomicLong(); + AtomicLong contentDurationMsRef = new AtomicLong(); + AtomicLong contentBufferedPositionMsRef = new AtomicLong(); + AtomicBoolean isPlayingAdRef = new AtomicBoolean(); + AtomicInteger currentAdGroupIndexRef = new AtomicInteger(); + AtomicInteger currentAdIndexInAdGroupRef = new AtomicInteger(); + AtomicInteger currentWindowIndexRef = new AtomicInteger(); + AtomicInteger currentPeriodIndexRef = new AtomicInteger(); + controller.addListener( + new SessionPlayer.PlayerCallback() { + @Override + public void onPositionDiscontinuity( + PositionInfo oldPosition, PositionInfo newPosition, @DiscontinuityReason int reason) { + currentPositionMsRef.set(controller.getCurrentPosition()); + contentPositionMsRef.set(controller.getContentPosition()); + durationMsRef.set(controller.getDuration()); + bufferedPositionMsRef.set(controller.getBufferedPosition()); + bufferedPercentageRef.set(controller.getBufferedPercentage()); + totalBufferedDurationMsRef.set(controller.getTotalBufferedDuration()); + currentLiveOffsetMsRef.set(controller.getCurrentLiveOffset()); + contentDurationMsRef.set(controller.getContentDuration()); + contentBufferedPositionMsRef.set(controller.getContentBufferedPosition()); + isPlayingAdRef.set(controller.isPlayingAd()); + currentAdGroupIndexRef.set(controller.getCurrentAdGroupIndex()); + currentAdIndexInAdGroupRef.set(controller.getCurrentAdIndexInAdGroup()); + currentWindowIndexRef.set(controller.getCurrentWindowIndex()); + currentPeriodIndexRef.set(controller.getCurrentPeriodIndex()); + latch.countDown(); + } + }); + + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setPlaybackState(Player.STATE_READY) + .setCurrentPosition(testCurrentPositionMs) + .setContentPosition(testContentPositionMs) + .setDuration(testDurationMs) + .setBufferedPosition(testBufferedPositionMs) + .setBufferedPercentage(testBufferedPercentage) + .setTotalBufferedDuration(testTotalBufferedDurationMs) + .setCurrentLiveOffset(testCurrentLiveOffsetMs) + .setContentDuration(testContentDurationMs) + .setContentBufferedPosition(testContentBufferedPositionMs) + .setIsPlayingAd(testIsPlayingAd) + .setCurrentAdGroupIndex(testCurrentAdGroupIndex) + .setCurrentAdIndexInAdGroup(testCurrentAdIndexInAdGroup) + .setCurrentWindowIndex(testWindowIndex) + .setCurrentPeriodIndex(testPeriodIndex) + .build(); + remoteSession.setPlayer(playerConfig); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(currentPositionMsRef.get()).isEqualTo(testCurrentPositionMs); + assertThat(contentPositionMsRef.get()).isEqualTo(testContentPositionMs); + assertThat(durationMsRef.get()).isEqualTo(testDurationMs); + assertThat(bufferedPositionMsRef.get()).isEqualTo(testBufferedPositionMs); + assertThat(bufferedPercentageRef.get()).isEqualTo(testBufferedPercentage); + assertThat(totalBufferedDurationMsRef.get()).isEqualTo(testTotalBufferedDurationMs); + assertThat(currentLiveOffsetMsRef.get()).isEqualTo(testCurrentLiveOffsetMs); + assertThat(contentDurationMsRef.get()).isEqualTo(testContentDurationMs); + assertThat(contentBufferedPositionMsRef.get()).isEqualTo(testContentBufferedPositionMs); + assertThat(isPlayingAdRef.get()).isEqualTo(testIsPlayingAd); + assertThat(currentAdGroupIndexRef.get()).isEqualTo(testCurrentAdGroupIndex); + assertThat(currentAdIndexInAdGroupRef.get()).isEqualTo(testCurrentAdIndexInAdGroup); + assertThat(currentWindowIndexRef.get()).isEqualTo(testWindowIndex); + assertThat(currentPeriodIndexRef.get()).isEqualTo(testPeriodIndex); + } + + @Test + public void onMediaItemTransition() throws Exception { + int currentIndex = 0; + Timeline timeline = MediaTestUtils.createTimeline(/* windowCount= */ 5); + remoteSession.getMockPlayer().setTimeline(timeline); + remoteSession.getMockPlayer().setCurrentWindowIndex(currentIndex); + remoteSession + .getMockPlayer() + .notifyMediaItemTransition( + currentIndex, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + + AtomicReference mediaItemFromParam = new AtomicReference<>(); + AtomicReference mediaItemFromGetter = new AtomicReference<>(); + AtomicInteger reasonRef = new AtomicInteger(); + CountDownLatch latch = new CountDownLatch(1); + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + controller.addListener( + new SessionPlayer.PlayerCallback() { + @Override + public void onMediaItemTransition( + @Nullable MediaItem mediaItem, @Player.MediaItemTransitionReason int reason) { + mediaItemFromParam.set(mediaItem); + mediaItemFromGetter.set(controller.getCurrentMediaItem()); + reasonRef.set(reason); + latch.countDown(); + } + }); + + int testIndex = 3; + int testReason = Player.MEDIA_ITEM_TRANSITION_REASON_SEEK; + remoteSession.getMockPlayer().setCurrentWindowIndex(testIndex); + remoteSession.getMockPlayer().notifyMediaItemTransition(testIndex, testReason); + Timeline.Window window = new Timeline.Window(); + MediaItem currentMediaItem = timeline.getWindow(testIndex, window).mediaItem; + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(mediaItemFromParam.get()).isEqualTo(currentMediaItem); + assertThat(mediaItemFromGetter.get()).isEqualTo(currentMediaItem); + assertThat(reasonRef.get()).isEqualTo(testReason); + } + + @Test + public void onMediaItemTransition_withNullMediaIteam() throws Exception { + Timeline timeline = MediaTestUtils.createTimeline(/* windowCount= */ 1); + remoteSession.getMockPlayer().setTimeline(timeline); + remoteSession.getMockPlayer().setCurrentWindowIndex(0); + remoteSession + .getMockPlayer() + .notifyMediaItemTransition(0, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + + AtomicReference mediaItemRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + controller.addListener( + new SessionPlayer.PlayerCallback() { + @Override + public void onMediaItemTransition( + @Nullable MediaItem mediaItem, @Player.MediaItemTransitionReason int reason) { + mediaItemRef.set(mediaItem); + latch.countDown(); + } + }); + + remoteSession.getMockPlayer().setTimeline(Timeline.EMPTY); + remoteSession + .getMockPlayer() + .notifyMediaItemTransition( + C.INDEX_UNSET, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(mediaItemRef.get()).isNull(); + } + + /** This also tests {@link MediaController#getPlaybackParameters()}. */ + @Test + public void onPlaybackParametersChanged_isNotified() throws Exception { + PlaybackParameters testPlaybackParameters = + new PlaybackParameters(/* speed= */ 3.2f, /* pitch= */ 2.1f); + remoteSession.getMockPlayer().setPlaybackParameters(PlaybackParameters.DEFAULT); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference playbackParametersRef = new AtomicReference<>(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + playbackParametersRef.set(playbackParameters); + latch.countDown(); + } + }; + controller.addListener(callback); + + remoteSession.getMockPlayer().notifyPlaybackParametersChanged(testPlaybackParameters); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(playbackParametersRef.get()).isEqualTo(testPlaybackParameters); + } + + @Test + public void onPlaybackParametersChanged_updatesGetters() throws Exception { + PlaybackParameters testPlaybackParameters = + new PlaybackParameters(/* speed= */ 3.2f, /* pitch= */ 2.1f); + long testCurrentPositionMs = 11; + long testContentPositionMs = 33; + long testBufferedPositionMs = 100; + int testBufferedPercentage = 50; + long testTotalBufferedDurationMs = 120; + long testCurrentLiveOffsetMs = 10; + long testContentBufferedPositionMs = 240; + + controller = controllerTestRule.createController(remoteSession.getToken()); + + CountDownLatch latch = new CountDownLatch(1); + AtomicReference playbackParametersRef = new AtomicReference<>(); + AtomicLong currentPositionMsRef = new AtomicLong(); + AtomicLong contentPositionMsRef = new AtomicLong(); + AtomicLong durationMsRef = new AtomicLong(); + AtomicLong bufferedPositionMsRef = new AtomicLong(); + AtomicInteger bufferedPercentageRef = new AtomicInteger(); + AtomicLong totalBufferedDurationMsRef = new AtomicLong(); + AtomicLong currentLiveOffsetMsRef = new AtomicLong(); + AtomicLong contentDurationMsRef = new AtomicLong(); + AtomicLong contentBufferedPositionMsRef = new AtomicLong(); + AtomicBoolean isPlayingAdRef = new AtomicBoolean(); + AtomicInteger currentAdGroupIndexRef = new AtomicInteger(); + AtomicInteger currentAdIndexInAdGroupRef = new AtomicInteger(); + controller.addListener( + new SessionPlayer.PlayerCallback() { + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + playbackParametersRef.set(controller.getPlaybackParameters()); + currentPositionMsRef.set(controller.getCurrentPosition()); + contentPositionMsRef.set(controller.getContentPosition()); + bufferedPositionMsRef.set(controller.getBufferedPosition()); + bufferedPercentageRef.set(controller.getBufferedPercentage()); + totalBufferedDurationMsRef.set(controller.getTotalBufferedDuration()); + currentLiveOffsetMsRef.set(controller.getCurrentLiveOffset()); + contentBufferedPositionMsRef.set(controller.getContentBufferedPosition()); + latch.countDown(); + } + }); + + remoteSession.getMockPlayer().setCurrentPosition(testCurrentPositionMs); + remoteSession.getMockPlayer().setContentPosition(testContentPositionMs); + remoteSession.getMockPlayer().setBufferedPosition(testBufferedPositionMs); + remoteSession.getMockPlayer().setBufferedPercentage(testBufferedPercentage); + remoteSession.getMockPlayer().setTotalBufferedDuration(testTotalBufferedDurationMs); + remoteSession.getMockPlayer().setCurrentLiveOffset(testCurrentLiveOffsetMs); + remoteSession.getMockPlayer().setContentBufferedPosition(testContentBufferedPositionMs); + remoteSession.getMockPlayer().notifyPlaybackParametersChanged(testPlaybackParameters); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(playbackParametersRef.get()).isEqualTo(testPlaybackParameters); + assertThat(currentPositionMsRef.get()).isEqualTo(testCurrentPositionMs); + assertThat(contentPositionMsRef.get()).isEqualTo(testContentPositionMs); + assertThat(bufferedPositionMsRef.get()).isEqualTo(testBufferedPositionMs); + assertThat(bufferedPercentageRef.get()).isEqualTo(testBufferedPercentage); + assertThat(totalBufferedDurationMsRef.get()).isEqualTo(testTotalBufferedDurationMs); + assertThat(currentLiveOffsetMsRef.get()).isEqualTo(testCurrentLiveOffsetMs); + assertThat(contentBufferedPositionMsRef.get()).isEqualTo(testContentBufferedPositionMs); + } + + /** This also tests {@link MediaController#getCurrentTimeline()}. */ + @Test + public void onTimelineChanged() throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference timelineFromParamRef = new AtomicReference<>(); + AtomicReference timelineFromGetterRef = new AtomicReference<>(); + AtomicInteger reasonRef = new AtomicInteger(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onTimelineChanged( + Timeline timeline, @Player.TimelineChangeReason int reason) { + timelineFromParamRef.set(timeline); + timelineFromGetterRef.set(controller.getCurrentTimeline()); + reasonRef.set(reason); + latch.countDown(); + } + }; + controller.addListener(callback); + + Timeline timeline = MediaTestUtils.createTimeline(/* windowCount= */ 2); + @Player.TimelineChangeReason int reason = Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE; + remoteSession.getMockPlayer().setTimeline(timeline); + remoteSession.getMockPlayer().notifyTimelineChanged(reason); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + MediaTestUtils.assertMediaIdEquals(timeline, timelineFromParamRef.get()); + MediaTestUtils.assertMediaIdEquals(timeline, timelineFromGetterRef.get()); + assertThat(reasonRef.get()).isEqualTo(reason); + } + + @Test + @LargeTest + public void onTimelineChanged_withLongPlaylist() throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + AtomicReference timelineRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onTimelineChanged( + Timeline timeline, @Player.TimelineChangeReason int reason) { + timelineRef.set(timeline); + latch.countDown(); + } + }; + controller.addListener(callback); + + int windowCount = 5_000; + remoteSession.getMockPlayer().createAndSetFakeTimeline(windowCount); + remoteSession + .getMockPlayer() + .notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + + assertThat(latch.await(LONG_TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(timelineRef.get().getWindowCount()).isEqualTo(windowCount); + Timeline.Window window = new Timeline.Window(); + for (int i = 0; i < windowCount; i++) { + assertThat(timelineRef.get().getWindow(i, window).mediaItem.mediaId) + .isEqualTo(TestUtils.getMediaIdInFakeTimeline(i)); + } + } + + @Test + public void onTimelineChanged_withEmptyTimeline() throws Exception { + remoteSession.getMockPlayer().createAndSetFakeTimeline(/* windowCount= */ 1); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference timelineRef = new AtomicReference<>(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onTimelineChanged( + Timeline timeline, @Player.TimelineChangeReason int reason) { + timelineRef.set(timeline); + latch.countDown(); + } + }; + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + controller.addListener(callback); + + remoteSession.getMockPlayer().setTimeline(Timeline.EMPTY); + remoteSession + .getMockPlayer() + .notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(timelineRef.get().getWindowCount()).isEqualTo(0); + assertThat(timelineRef.get().getPeriodCount()).isEqualTo(0); + } + + /** This also tests {@link MediaController#getPlaylistMetadata()}. */ + @Test + public void onPlaylistMetadataChanged() throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + AtomicReference metadataFromParamRef = new AtomicReference<>(); + AtomicReference metadataFromGetterRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPlaylistMetadataChanged(MediaMetadata metadata) { + metadataFromParamRef.set(metadata); + metadataFromGetterRef.set(controller.getPlaylistMetadata()); + latch.countDown(); + } + }; + controller.addListener(callback); + + MediaMetadata playlistMetadata = new MediaMetadata.Builder().setTitle("title").build(); + RemoteMediaSession.RemoteMockPlayer player = remoteSession.getMockPlayer(); + player.setPlaylistMetadata(playlistMetadata); + player.notifyPlaylistMetadataChanged(); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(metadataFromParamRef.get()).isEqualTo(playlistMetadata); + assertThat(metadataFromGetterRef.get()).isEqualTo(playlistMetadata); + } + + /** This also tests {@link MediaController#getShuffleModeEnabled()}. */ + @Test + public void onShuffleModeEnabledChanged() throws Exception { + RemoteMediaSession.RemoteMockPlayer player = remoteSession.getMockPlayer(); + Timeline timeline = + new PlaylistTimeline( + MediaTestUtils.createConvergedMediaItems(/* size= */ 3), + /* shuffledIndices= */ new int[] {0, 2, 1}); + player.setTimeline(timeline); + player.notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + player.setCurrentWindowIndex(2); + player.setShuffleModeEnabled(false); + player.notifyShuffleModeEnabledChanged(); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean shuffleModeEnabledFromParamRef = new AtomicBoolean(); + AtomicBoolean shuffleModeEnabledFromGetterRef = new AtomicBoolean(); + AtomicInteger previousIndexRef = new AtomicInteger(); + AtomicInteger nextIndexRef = new AtomicInteger(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + shuffleModeEnabledFromParamRef.set(shuffleModeEnabled); + shuffleModeEnabledFromGetterRef.set(controller.getShuffleModeEnabled()); + previousIndexRef.set(controller.getPreviousWindowIndex()); + nextIndexRef.set(controller.getNextWindowIndex()); + latch.countDown(); + } + }; + controller.addListener(callback); + + player.setShuffleModeEnabled(true); + player.notifyShuffleModeEnabledChanged(); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(shuffleModeEnabledFromParamRef.get()).isTrue(); + assertThat(shuffleModeEnabledFromGetterRef.get()).isTrue(); + assertThat(previousIndexRef.get()).isEqualTo(0); + assertThat(nextIndexRef.get()).isEqualTo(1); + } + + /** This also tests {@link MediaController#getRepeatMode()}. */ + @Test + public void onRepeatModeChanged() throws Exception { + RemoteMediaSession.RemoteMockPlayer player = remoteSession.getMockPlayer(); + Timeline timeline = MediaTestUtils.createTimeline(/* windowCount= */ 3); + player.setTimeline(timeline); + player.notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + player.setCurrentWindowIndex(2); + player.setRepeatMode(Player.REPEAT_MODE_OFF); + player.notifyRepeatModeChanged(); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger repeatModeFromParamRef = new AtomicInteger(); + AtomicInteger repeatModeFromGetterRef = new AtomicInteger(); + AtomicInteger previousIndexRef = new AtomicInteger(); + AtomicInteger nextIndexRef = new AtomicInteger(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onRepeatModeChanged(@RepeatMode int repeatMode) { + repeatModeFromParamRef.set(repeatMode); + repeatModeFromGetterRef.set(controller.getRepeatMode()); + previousIndexRef.set(controller.getPreviousWindowIndex()); + nextIndexRef.set(controller.getNextWindowIndex()); + latch.countDown(); + } + }; + controller.addListener(callback); + + int testRepeatMode = REPEAT_MODE_ALL; + player.setRepeatMode(testRepeatMode); + player.notifyRepeatModeChanged(); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(repeatModeFromParamRef.get()).isEqualTo(testRepeatMode); + assertThat(repeatModeFromGetterRef.get()).isEqualTo(testRepeatMode); + assertThat(previousIndexRef.get()).isEqualTo(1); + assertThat(nextIndexRef.get()).isEqualTo(0); + } + + @Test + public void onPlayWhenReadyChanged_isNotified() throws Exception { + boolean testPlayWhenReady = true; + @Player.PlayWhenReadyChangeReason + int testReason = Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST; + @Player.PlaybackSuppressionReason + int testSuppressionReason = Player.PLAYBACK_SUPPRESSION_REASON_NONE; + remoteSession + .getMockPlayer() + .setPlayWhenReady(false, Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + CountDownLatch latch = new CountDownLatch(2); + AtomicBoolean playWhenReadyRef = new AtomicBoolean(); + AtomicInteger playWhenReadyReasonRef = new AtomicInteger(); + AtomicInteger playbackSuppressionReasonRef = new AtomicInteger(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) { + playWhenReadyRef.set(playWhenReady); + playWhenReadyReasonRef.set(reason); + latch.countDown(); + } + + @Override + public void onPlaybackSuppressionReasonChanged(int playbackSuppressionReason) { + playbackSuppressionReasonRef.set(playbackSuppressionReason); + latch.countDown(); + } + }; + controller.addListener(callback); + + remoteSession.getMockPlayer().notifyPlayWhenReadyChanged(testPlayWhenReady, testReason); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(playWhenReadyRef.get()).isEqualTo(testPlayWhenReady); + assertThat(playWhenReadyReasonRef.get()).isEqualTo(testReason); + assertThat(playbackSuppressionReasonRef.get()).isEqualTo(testSuppressionReason); + } + + @Test + public void onPlayWhenReadyChanged_updatesGetters() throws Exception { + boolean testPlayWhenReady = true; + @Player.PlaybackSuppressionReason int testReason = Player.PLAYBACK_SUPPRESSION_REASON_NONE; + remoteSession + .getMockPlayer() + .setPlayWhenReady(false, Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS); + long testCurrentPositionMs = 11; + long testContentPositionMs = 33; + long testBufferedPositionMs = 100; + int testBufferedPercentage = 50; + long testTotalBufferedDurationMs = 120; + long testCurrentLiveOffsetMs = 10; + long testContentBufferedPositionMs = 240; + + controller = controllerTestRule.createController(remoteSession.getToken()); + + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean playWhenReadyRef = new AtomicBoolean(); + AtomicInteger playbackSuppressionReasonRef = new AtomicInteger(); + AtomicLong currentPositionMsRef = new AtomicLong(); + AtomicLong contentPositionMsRef = new AtomicLong(); + AtomicLong bufferedPositionMsRef = new AtomicLong(); + AtomicInteger bufferedPercentageRef = new AtomicInteger(); + AtomicLong totalBufferedDurationMsRef = new AtomicLong(); + AtomicLong currentLiveOffsetMsRef = new AtomicLong(); + AtomicLong contentBufferedPositionMsRef = new AtomicLong(); + controller.addListener( + new SessionPlayer.PlayerCallback() { + @Override + public void onPlayWhenReadyChanged( + boolean playWhenReady, @PlayWhenReadyChangeReason int reason) { + playWhenReadyRef.set(controller.getPlayWhenReady()); + playbackSuppressionReasonRef.set(controller.getPlaybackSuppressionReason()); + currentPositionMsRef.set(controller.getCurrentPosition()); + contentPositionMsRef.set(controller.getContentPosition()); + bufferedPositionMsRef.set(controller.getBufferedPosition()); + bufferedPercentageRef.set(controller.getBufferedPercentage()); + totalBufferedDurationMsRef.set(controller.getTotalBufferedDuration()); + currentLiveOffsetMsRef.set(controller.getCurrentLiveOffset()); + contentBufferedPositionMsRef.set(controller.getContentBufferedPosition()); + latch.countDown(); + } + }); + + remoteSession.getMockPlayer().setCurrentPosition(testCurrentPositionMs); + remoteSession.getMockPlayer().setContentPosition(testContentPositionMs); + remoteSession.getMockPlayer().setBufferedPosition(testBufferedPositionMs); + remoteSession.getMockPlayer().setBufferedPercentage(testBufferedPercentage); + remoteSession.getMockPlayer().setTotalBufferedDuration(testTotalBufferedDurationMs); + remoteSession.getMockPlayer().setCurrentLiveOffset(testCurrentLiveOffsetMs); + remoteSession.getMockPlayer().setContentBufferedPosition(testContentBufferedPositionMs); + remoteSession.getMockPlayer().notifyPlayWhenReadyChanged(testPlayWhenReady, testReason); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(playWhenReadyRef.get()).isEqualTo(testPlayWhenReady); + assertThat(playbackSuppressionReasonRef.get()).isEqualTo(testReason); + assertThat(currentPositionMsRef.get()).isEqualTo(testCurrentPositionMs); + assertThat(bufferedPositionMsRef.get()).isEqualTo(testBufferedPositionMs); + assertThat(bufferedPercentageRef.get()).isEqualTo(testBufferedPercentage); + assertThat(totalBufferedDurationMsRef.get()).isEqualTo(testTotalBufferedDurationMs); + assertThat(currentLiveOffsetMsRef.get()).isEqualTo(testCurrentLiveOffsetMs); + assertThat(contentBufferedPositionMsRef.get()).isEqualTo(testContentBufferedPositionMs); + } + + @Test + public void onPlaybackSuppressionReasonChanged_isNotified() throws Exception { + boolean testPlayWhenReady = true; + @Player.PlaybackSuppressionReason + int testReason = Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS; + remoteSession + .getMockPlayer() + .setPlayWhenReady(testPlayWhenReady, Player.PLAYBACK_SUPPRESSION_REASON_NONE); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger playbackSuppressionReasonRef = new AtomicInteger(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPlaybackSuppressionReasonChanged(int reason) { + playbackSuppressionReasonRef.set(reason); + latch.countDown(); + } + }; + controller.addListener(callback); + + remoteSession.getMockPlayer().notifyPlayWhenReadyChanged(testPlayWhenReady, testReason); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(playbackSuppressionReasonRef.get()).isEqualTo(testReason); + } + + @Test + public void onPlaybackSuppressionReasonChanged_updatesGetters() throws Exception { + boolean testPlayWhenReady = true; + @Player.PlaybackSuppressionReason + int testReason = Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS; + long testCurrentPositionMs = 11; + long testContentPositionMs = 33; + long testBufferedPositionMs = 100; + int testBufferedPercentage = 50; + long testTotalBufferedDurationMs = 120; + long testCurrentLiveOffsetMs = 10; + long testContentBufferedPositionMs = 240; + remoteSession + .getMockPlayer() + .setPlayWhenReady(testPlayWhenReady, Player.PLAYBACK_SUPPRESSION_REASON_NONE); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger playbackSuppressionReasonRef = new AtomicInteger(); + AtomicLong currentPositionMsRef = new AtomicLong(); + AtomicLong contentPositionMsRef = new AtomicLong(); + AtomicLong bufferedPositionMsRef = new AtomicLong(); + AtomicInteger bufferedPercentageRef = new AtomicInteger(); + AtomicLong totalBufferedDurationMsRef = new AtomicLong(); + AtomicLong currentLiveOffsetMsRef = new AtomicLong(); + AtomicLong contentBufferedPositionMsRef = new AtomicLong(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPlaybackSuppressionReasonChanged(int reason) { + playbackSuppressionReasonRef.set(controller.getPlaybackSuppressionReason()); + currentPositionMsRef.set(controller.getCurrentPosition()); + contentPositionMsRef.set(controller.getContentPosition()); + bufferedPositionMsRef.set(controller.getBufferedPosition()); + bufferedPercentageRef.set(controller.getBufferedPercentage()); + totalBufferedDurationMsRef.set(controller.getTotalBufferedDuration()); + currentLiveOffsetMsRef.set(controller.getCurrentLiveOffset()); + contentBufferedPositionMsRef.set(controller.getContentBufferedPosition()); + latch.countDown(); + } + }; + controller.addListener(callback); + + remoteSession.getMockPlayer().setCurrentPosition(testCurrentPositionMs); + remoteSession.getMockPlayer().setContentPosition(testContentPositionMs); + remoteSession.getMockPlayer().setBufferedPosition(testBufferedPositionMs); + remoteSession.getMockPlayer().setBufferedPercentage(testBufferedPercentage); + remoteSession.getMockPlayer().setTotalBufferedDuration(testTotalBufferedDurationMs); + remoteSession.getMockPlayer().setCurrentLiveOffset(testCurrentLiveOffsetMs); + remoteSession.getMockPlayer().setContentBufferedPosition(testContentBufferedPositionMs); + remoteSession.getMockPlayer().notifyPlayWhenReadyChanged(testPlayWhenReady, testReason); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(playbackSuppressionReasonRef.get()).isEqualTo(testReason); + assertThat(currentPositionMsRef.get()).isEqualTo(testCurrentPositionMs); + assertThat(contentPositionMsRef.get()).isEqualTo(testContentPositionMs); + assertThat(bufferedPositionMsRef.get()).isEqualTo(testBufferedPositionMs); + assertThat(bufferedPercentageRef.get()).isEqualTo(testBufferedPercentage); + assertThat(totalBufferedDurationMsRef.get()).isEqualTo(testTotalBufferedDurationMs); + assertThat(currentLiveOffsetMsRef.get()).isEqualTo(testCurrentLiveOffsetMs); + assertThat(contentBufferedPositionMsRef.get()).isEqualTo(testContentBufferedPositionMs); + } + + @Test + public void onPlaybackStateChanged_isNotified() throws Exception { + @Player.State int testPlaybackState = STATE_BUFFERING; + remoteSession.getMockPlayer().notifyPlaybackStateChanged(Player.STATE_IDLE); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger playbackStateRef = new AtomicInteger(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPlaybackStateChanged(int reason) { + playbackStateRef.set(reason); + latch.countDown(); + } + }; + controller.addListener(callback); + + remoteSession.getMockPlayer().notifyPlaybackStateChanged(testPlaybackState); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(playbackStateRef.get()).isEqualTo(testPlaybackState); + } + + @Test + public void onPlaybackStateChanged_updatesGetters() throws Exception { + @Player.State int testPlaybackState = STATE_BUFFERING; + long testCurrentPositionMs = 11; + long testContentPositionMs = 33; + long testBufferedPositionMs = 100; + int testBufferedPercentage = 50; + long testTotalBufferedDurationMs = 120; + long testCurrentLiveOffsetMs = 10; + long testContentBufferedPositionMs = 240; + remoteSession.getMockPlayer().notifyPlaybackStateChanged(Player.STATE_IDLE); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger playbackStateRef = new AtomicInteger(); + AtomicLong currentPositionMsRef = new AtomicLong(); + AtomicLong contentPositionMsRef = new AtomicLong(); + AtomicLong bufferedPositionMsRef = new AtomicLong(); + AtomicInteger bufferedPercentageRef = new AtomicInteger(); + AtomicLong totalBufferedDurationMsRef = new AtomicLong(); + AtomicLong currentLiveOffsetMsRef = new AtomicLong(); + AtomicLong contentBufferedPositionMsRef = new AtomicLong(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPlaybackStateChanged(int reason) { + playbackStateRef.set(controller.getPlaybackState()); + currentPositionMsRef.set(controller.getCurrentPosition()); + contentPositionMsRef.set(controller.getContentPosition()); + bufferedPositionMsRef.set(controller.getBufferedPosition()); + bufferedPercentageRef.set(controller.getBufferedPercentage()); + totalBufferedDurationMsRef.set(controller.getTotalBufferedDuration()); + currentLiveOffsetMsRef.set(controller.getCurrentLiveOffset()); + contentBufferedPositionMsRef.set(controller.getContentBufferedPosition()); + latch.countDown(); + } + }; + controller.addListener(callback); + + remoteSession.getMockPlayer().setCurrentPosition(testCurrentPositionMs); + remoteSession.getMockPlayer().setContentPosition(testContentPositionMs); + remoteSession.getMockPlayer().setBufferedPosition(testBufferedPositionMs); + remoteSession.getMockPlayer().setBufferedPercentage(testBufferedPercentage); + remoteSession.getMockPlayer().setTotalBufferedDuration(testTotalBufferedDurationMs); + remoteSession.getMockPlayer().setCurrentLiveOffset(testCurrentLiveOffsetMs); + remoteSession.getMockPlayer().setContentBufferedPosition(testContentBufferedPositionMs); + remoteSession.getMockPlayer().notifyPlaybackStateChanged(testPlaybackState); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(playbackStateRef.get()).isEqualTo(testPlaybackState); + assertThat(currentPositionMsRef.get()).isEqualTo(testCurrentPositionMs); + assertThat(contentPositionMsRef.get()).isEqualTo(testContentPositionMs); + assertThat(bufferedPositionMsRef.get()).isEqualTo(testBufferedPositionMs); + assertThat(bufferedPercentageRef.get()).isEqualTo(testBufferedPercentage); + assertThat(totalBufferedDurationMsRef.get()).isEqualTo(testTotalBufferedDurationMs); + assertThat(currentLiveOffsetMsRef.get()).isEqualTo(testCurrentLiveOffsetMs); + assertThat(contentBufferedPositionMsRef.get()).isEqualTo(testContentBufferedPositionMs); + } + + @Test + public void onIsPlayingChanged_isNotified() throws Exception { + boolean testIsPlaying = true; + remoteSession.getMockPlayer().notifyIsPlayingChanged(false); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean isPlayingRef = new AtomicBoolean(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onIsPlayingChanged(boolean isPlaying) { + isPlayingRef.set(isPlaying); + latch.countDown(); + } + }; + controller.addListener(callback); + + remoteSession.getMockPlayer().notifyIsPlayingChanged(testIsPlaying); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(isPlayingRef.get()).isEqualTo(testIsPlaying); + } + + @Test + public void onIsPlayingChanged_updatesGetters() throws Exception { + boolean testIsPlaying = true; + long testCurrentPositionMs = 11; + long testContentPositionMs = 33; + long testBufferedPositionMs = 100; + int testBufferedPercentage = 50; + long testTotalBufferedDurationMs = 120; + long testCurrentLiveOffsetMs = 10; + long testContentBufferedPositionMs = 240; + remoteSession.getMockPlayer().notifyIsPlayingChanged(false); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + threadTestRule.getHandler().postAndSync(() -> controller.setTimeDiffMs(/* timeDiff= */ 0L)); + + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean isPlayingRef = new AtomicBoolean(); + AtomicLong currentPositionMsRef = new AtomicLong(); + AtomicLong contentPositionMsRef = new AtomicLong(); + AtomicLong bufferedPositionMsRef = new AtomicLong(); + AtomicInteger bufferedPercentageRef = new AtomicInteger(); + AtomicLong totalBufferedDurationMsRef = new AtomicLong(); + AtomicLong currentLiveOffsetMsRef = new AtomicLong(); + AtomicLong contentBufferedPositionMsRef = new AtomicLong(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onIsPlayingChanged(boolean isPlaying) { + isPlayingRef.set(controller.isPlaying()); + currentPositionMsRef.set(controller.getCurrentPosition()); + contentPositionMsRef.set(controller.getContentPosition()); + bufferedPositionMsRef.set(controller.getBufferedPosition()); + bufferedPercentageRef.set(controller.getBufferedPercentage()); + totalBufferedDurationMsRef.set(controller.getTotalBufferedDuration()); + currentLiveOffsetMsRef.set(controller.getCurrentLiveOffset()); + contentBufferedPositionMsRef.set(controller.getContentBufferedPosition()); + latch.countDown(); + } + }; + controller.addListener(callback); + + remoteSession.getMockPlayer().setCurrentPosition(testCurrentPositionMs); + remoteSession.getMockPlayer().setContentPosition(testContentPositionMs); + remoteSession.getMockPlayer().setBufferedPosition(testBufferedPositionMs); + remoteSession.getMockPlayer().setBufferedPercentage(testBufferedPercentage); + remoteSession.getMockPlayer().setTotalBufferedDuration(testTotalBufferedDurationMs); + remoteSession.getMockPlayer().setCurrentLiveOffset(testCurrentLiveOffsetMs); + remoteSession.getMockPlayer().setContentBufferedPosition(testContentBufferedPositionMs); + remoteSession.getMockPlayer().notifyIsPlayingChanged(testIsPlaying); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(isPlayingRef.get()).isEqualTo(testIsPlaying); + assertThat(currentPositionMsRef.get()).isEqualTo(testCurrentPositionMs); + assertThat(contentPositionMsRef.get()).isEqualTo(testContentPositionMs); + assertThat(bufferedPositionMsRef.get()).isEqualTo(testBufferedPositionMs); + assertThat(bufferedPercentageRef.get()).isEqualTo(testBufferedPercentage); + assertThat(totalBufferedDurationMsRef.get()).isEqualTo(testTotalBufferedDurationMs); + assertThat(currentLiveOffsetMsRef.get()).isEqualTo(testCurrentLiveOffsetMs); + assertThat(contentBufferedPositionMsRef.get()).isEqualTo(testContentBufferedPositionMs); + } + + @Test + public void onIsLoadingChanged_isNotified() throws Exception { + boolean testIsLoading = true; + remoteSession.getMockPlayer().notifyIsLoadingChanged(false); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean isLoadingFromParamRef = new AtomicBoolean(); + AtomicBoolean isLoadingFromGetterRef = new AtomicBoolean(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onIsLoadingChanged(boolean isLoading) { + isLoadingFromParamRef.set(isLoading); + isLoadingFromGetterRef.set(controller.isLoading()); + latch.countDown(); + } + }; + controller.addListener(callback); + + remoteSession.getMockPlayer().notifyIsLoadingChanged(testIsLoading); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(isLoadingFromParamRef.get()).isEqualTo(testIsLoading); + assertThat(isLoadingFromGetterRef.get()).isEqualTo(testIsLoading); + } + + @Test + public void onPositionDiscontinuity_isNotified() throws Exception { + PositionInfo testOldPosition = + new PositionInfo( + /* windowUid= */ null, + /* windowIndex= */ 2, + /* periodUid= */ null, + /* periodIndex= */ C.INDEX_UNSET, + /* positionMs= */ 300L, + /* contentPositionMs= */ 200L, + /* adGroupIndex= */ 33, + /* adIndexInAdGroup= */ 2); + PositionInfo testNewPosition = + new PositionInfo( + /* windowUid= */ null, + /* windowIndex= */ 3, + /* periodUid= */ null, + /* periodIndex= */ C.INDEX_UNSET, + /* positionMs= */ 0L, + /* contentPositionMs= */ 0L, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET); + @DiscontinuityReason int testReason = Player.DISCONTINUITY_REASON_INTERNAL; + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference oldPositionRef = new AtomicReference<>(); + AtomicReference newPositionRef = new AtomicReference<>(); + AtomicInteger positionDiscontinuityReasonRef = new AtomicInteger(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPositionDiscontinuity( + PositionInfo oldPosition, PositionInfo newPosition, @DiscontinuityReason int reason) { + oldPositionRef.set(oldPosition); + newPositionRef.set(newPosition); + positionDiscontinuityReasonRef.set(reason); + latch.countDown(); + } + }; + controller.addListener(callback); + + remoteSession + .getMockPlayer() + .notifyPositionDiscontinuity(testOldPosition, testNewPosition, testReason); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(positionDiscontinuityReasonRef.get()).isEqualTo(testReason); + assertThat(oldPositionRef.get()).isEqualTo(testOldPosition); + assertThat(newPositionRef.get()).isEqualTo(testNewPosition); + } + + @Test + public void onPositionDiscontinuity_updatesGetters() throws Exception { + long testCurrentPositionMs = 11; + long testContentPositionMs = 33; + long testDurationMs = 200; + long testBufferedPositionMs = 100; + int testBufferedPercentage = 50; + long testTotalBufferedDurationMs = 120; + long testCurrentLiveOffsetMs = 10; + long testContentDurationMs = 300; + long testContentBufferedPositionMs = 240; + boolean testIsPlayingAd = true; + int testCurrentAdGroupIndex = 33; + int testCurrentAdIndexInAdGroup = 11; + PositionInfo newPositionInfo = + new PositionInfo( + /* windowUid= */ null, + /* windowIndex= */ C.INDEX_UNSET, + /* periodUid= */ null, + /* periodIndex= */ C.INDEX_UNSET, + testCurrentPositionMs, + testContentPositionMs, + testCurrentAdGroupIndex, + testCurrentAdIndexInAdGroup); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicLong currentPositionMsRef = new AtomicLong(); + AtomicLong contentPositionMsRef = new AtomicLong(); + AtomicLong durationMsRef = new AtomicLong(); + AtomicLong bufferedPositionMsRef = new AtomicLong(); + AtomicInteger bufferedPercentageRef = new AtomicInteger(); + AtomicLong totalBufferedDurationMsRef = new AtomicLong(); + AtomicLong currentLiveOffsetMsRef = new AtomicLong(); + AtomicLong contentDurationMsRef = new AtomicLong(); + AtomicLong contentBufferedPositionMsRef = new AtomicLong(); + AtomicBoolean isPlayingAdRef = new AtomicBoolean(); + AtomicInteger currentAdGroupIndexRef = new AtomicInteger(); + AtomicInteger currentAdIndexInAdGroupRef = new AtomicInteger(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPositionDiscontinuity( + PositionInfo oldPosition, PositionInfo newPosition, @DiscontinuityReason int reason) { + currentPositionMsRef.set(controller.getCurrentPosition()); + contentPositionMsRef.set(controller.getContentPosition()); + durationMsRef.set(controller.getDuration()); + bufferedPositionMsRef.set(controller.getBufferedPosition()); + bufferedPercentageRef.set(controller.getBufferedPercentage()); + totalBufferedDurationMsRef.set(controller.getTotalBufferedDuration()); + currentLiveOffsetMsRef.set(controller.getCurrentLiveOffset()); + contentDurationMsRef.set(controller.getContentDuration()); + contentBufferedPositionMsRef.set(controller.getContentBufferedPosition()); + isPlayingAdRef.set(controller.isPlayingAd()); + currentAdGroupIndexRef.set(controller.getCurrentAdGroupIndex()); + currentAdIndexInAdGroupRef.set(controller.getCurrentAdIndexInAdGroup()); + latch.countDown(); + } + }; + controller.addListener(callback); + + RemoteMockPlayer remoteMockPlayer = remoteSession.getMockPlayer(); + remoteMockPlayer.setCurrentPosition(testCurrentPositionMs); + remoteMockPlayer.setContentPosition(testContentPositionMs); + remoteMockPlayer.setDuration(testDurationMs); + remoteMockPlayer.setBufferedPosition(testBufferedPositionMs); + remoteMockPlayer.setBufferedPercentage(testBufferedPercentage); + remoteMockPlayer.setTotalBufferedDuration(testTotalBufferedDurationMs); + remoteMockPlayer.setCurrentLiveOffset(testCurrentLiveOffsetMs); + remoteMockPlayer.setContentDuration(testContentDurationMs); + remoteMockPlayer.setContentBufferedPosition(testContentBufferedPositionMs); + remoteMockPlayer.setIsPlayingAd(testIsPlayingAd); + remoteMockPlayer.setCurrentAdGroupIndex(testCurrentAdGroupIndex); + remoteMockPlayer.setCurrentAdIndexInAdGroup(testCurrentAdIndexInAdGroup); + remoteMockPlayer.notifyPositionDiscontinuity( + /* oldPositionInfo= */ SessionPositionInfo.DEFAULT_POSITION_INFO, + newPositionInfo, + Player.DISCONTINUITY_REASON_INTERNAL); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(currentPositionMsRef.get()).isEqualTo(testCurrentPositionMs); + assertThat(contentPositionMsRef.get()).isEqualTo(testContentPositionMs); + assertThat(durationMsRef.get()).isEqualTo(testDurationMs); + assertThat(bufferedPositionMsRef.get()).isEqualTo(testBufferedPositionMs); + assertThat(bufferedPercentageRef.get()).isEqualTo(testBufferedPercentage); + assertThat(totalBufferedDurationMsRef.get()).isEqualTo(testTotalBufferedDurationMs); + assertThat(currentLiveOffsetMsRef.get()).isEqualTo(testCurrentLiveOffsetMs); + assertThat(contentDurationMsRef.get()).isEqualTo(testContentDurationMs); + assertThat(contentBufferedPositionMsRef.get()).isEqualTo(testContentBufferedPositionMs); + assertThat(isPlayingAdRef.get()).isEqualTo(testIsPlayingAd); + assertThat(currentAdGroupIndexRef.get()).isEqualTo(testCurrentAdGroupIndex); + assertThat(currentAdIndexInAdGroupRef.get()).isEqualTo(testCurrentAdIndexInAdGroup); + } + + /** This also tests {@link MediaController#getAvailableSessionCommands()}. */ + @Test + public void onAvailableSessionCommandsChanged() throws Exception { + SessionCommands commands = + new SessionCommands.Builder() + .add(new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING)) + .build(); + + CountDownLatch latch = new CountDownLatch(1); + MediaController.ControllerCallback callback = + new MediaController.ControllerCallback() { + @Override + public void onAvailableSessionCommandsChanged( + MediaController controller, SessionCommands commandsOut) { + assertThat(commandsOut).isEqualTo(commands); + latch.countDown(); + } + }; + + MediaController controller = + controllerTestRule.createController(remoteSession.getToken(), true, null, callback); + remoteSession.setAvailableCommands(commands, Player.Commands.EMPTY); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(controller.getAvailableSessionCommands()).isEqualTo(commands); + } + + /** This also tests {@link MediaController#getAvailableCommands()}. */ + @Test + public void onAvailableCommandsChanged_isCalledByPlayerChange() throws Exception { + Commands commandsWithAllCommands = createPlayerCommandsWithAllCommands(); + Commands commandsWithSetRepeat = createPlayerCommandsWith(COMMAND_SET_REPEAT_MODE); + + remoteSession.getMockPlayer().notifyAvailableCommandsChanged(commandsWithAllCommands); + MediaController controller = + controllerTestRule.createController(remoteSession.getToken(), true, null, null); + + CountDownLatch latch = new CountDownLatch(1); + AtomicReference availableCommandsRef = new AtomicReference<>(); + AtomicReference availableCommandsFromGetterRef = new AtomicReference<>(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onAvailableCommandsChanged(Commands availableCommands) { + availableCommandsRef.set(availableCommands); + availableCommandsFromGetterRef.set(controller.getAvailableCommands()); + latch.countDown(); + } + }; + controller.addListener(callback); + + remoteSession.getMockPlayer().notifyAvailableCommandsChanged(commandsWithSetRepeat); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(availableCommandsRef.get()).isEqualTo(commandsWithSetRepeat); + assertThat(availableCommandsFromGetterRef.get()).isEqualTo(commandsWithSetRepeat); + } + + /** This also tests {@link MediaController#getAvailableCommands()}. */ + @Test + public void onAvailableCommandsChanged_isCalledBySessionChange() throws Exception { + Commands commandsWithAllCommands = createPlayerCommandsWithAllCommands(); + Commands commandsWithSetRepeat = createPlayerCommandsWith(COMMAND_SET_REPEAT_MODE); + + remoteSession.getMockPlayer().notifyAvailableCommandsChanged(commandsWithAllCommands); + MediaController controller = + controllerTestRule.createController(remoteSession.getToken(), true, null, null); + + CountDownLatch latch = new CountDownLatch(1); + AtomicReference availableCommandsRef = new AtomicReference<>(); + AtomicReference availableCommandsFromGetterRef = new AtomicReference<>(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onAvailableCommandsChanged(Commands availableCommands) { + availableCommandsRef.set(availableCommands); + availableCommandsFromGetterRef.set(controller.getAvailableCommands()); + latch.countDown(); + } + }; + controller.addListener(callback); + + remoteSession.setAvailableCommands(SessionCommands.EMPTY, commandsWithSetRepeat); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(availableCommandsRef.get()).isEqualTo(commandsWithSetRepeat); + assertThat(availableCommandsFromGetterRef.get()).isEqualTo(commandsWithSetRepeat); + } + + @Test + public void onCustomCommand() throws Exception { + String testCommandAction = "test_action"; + SessionCommand testCommand = new SessionCommand(testCommandAction, null); + Bundle testArgs = TestUtils.createTestBundle(); + + CountDownLatch latch = new CountDownLatch(2); + MediaController.ControllerCallback callback = + new MediaController.ControllerCallback() { + @Override + @NonNull + public ListenableFuture onCustomCommand( + @NonNull MediaController controller, @NonNull SessionCommand command, Bundle args) { + assertThat(command).isEqualTo(testCommand); + assertThat(TestUtils.equals(testArgs, args)).isTrue(); + latch.countDown(); + return new SessionResult(RESULT_SUCCESS).asFuture(); + } + }; + controllerTestRule.createController( + remoteSession.getToken(), + /* waitForConnect= */ true, + /* connectionHints= */ null, + callback); + + // TODO(jaewan): Test with multiple controllers + remoteSession.broadcastCustomCommand(testCommand, testArgs); + + // TODO(jaewan): Test receivers as well. + remoteSession.sendCustomCommand(testCommand, testArgs); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void onCustomLayoutChanged() throws Exception { + List buttons = new ArrayList<>(); + + CommandButton button = + new CommandButton.Builder() + .setPlayerCommand(COMMAND_PLAY_PAUSE) + .setDisplayName("button") + .build(); + buttons.add(button); + + CountDownLatch latch = new CountDownLatch(1); + MediaController.ControllerCallback callback = + new MediaController.ControllerCallback() { + @Override + @NonNull + public ListenableFuture onSetCustomLayout( + @NonNull MediaController controller, @NonNull List layout) { + assertThat(layout).hasSize(buttons.size()); + for (int i = 0; i < layout.size(); i++) { + assertThat(layout.get(i).playerCommand).isEqualTo(buttons.get(i).playerCommand); + assertThat(layout.get(i).displayName.toString()) + .isEqualTo(buttons.get(i).displayName.toString()); + } + latch.countDown(); + return new SessionResult(RESULT_SUCCESS).asFuture(); + } + }; + controllerTestRule.createController( + remoteSession.getToken(), + /* waitForConnect= */ true, + /* connectionHints= */ null, + callback); + remoteSession.setCustomLayout(buttons); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void onVideoSizeChanged() throws Exception { + VideoSize testVideoSize = + new VideoSize( + /* width= */ 100, + /* height= */ 42, + /* unappliedRotationDegrees= */ 90, + /* pixelWidthHeightRatio= */ 1.2f); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference videoSizeFromParamRef = new AtomicReference<>(); + AtomicReference videoSizeFromGetterRef = new AtomicReference<>(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onVideoSizeChanged(@NonNull VideoSize videoSize) { + videoSizeFromParamRef.set(videoSize); + videoSizeFromGetterRef.set(controller.getVideoSize()); + latch.countDown(); + } + }; + controller.addListener(callback); + + remoteSession.getMockPlayer().notifyVideoSizeChanged(testVideoSize); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(videoSizeFromParamRef.get()).isEqualTo(testVideoSize); + assertThat(videoSizeFromGetterRef.get()).isEqualTo(testVideoSize); + } + + @Test + public void onAudioAttributesChanged_isCalledAndUpdatesGetter() throws Exception { + AudioAttributes testAttributes = + new AudioAttributes.Builder() + .setUsage(C.USAGE_MEDIA) + .setContentType(C.CONTENT_TYPE_MOVIE) + .build(); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference attributesRef = new AtomicReference<>(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onAudioAttributesChanged(@NonNull AudioAttributes attributes) { + if (testAttributes.equals(attributes)) { + attributesRef.set(controller.getAudioAttributes()); + latch.countDown(); + } + } + }; + controller.addListener(callback); + + remoteSession.getMockPlayer().notifyAudioAttributesChanged(testAttributes); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(attributesRef.get()).isEqualTo(testAttributes); + } + + @Test + public void onDeviceInfoChanged_isCalledByPlayerChange() throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + AtomicReference deviceInfoFromParamRef = new AtomicReference<>(); + AtomicReference deviceInfoFromGetterRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onDeviceInfoChanged(@NonNull DeviceInfo deviceInfo) { + deviceInfoFromParamRef.set(deviceInfo); + deviceInfoFromGetterRef.set(controller.getDeviceInfo()); + latch.countDown(); + } + }; + controller.addListener(callback); + + DeviceInfo deviceInfo = + new DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, /* minVolume= */ 0, /* maxVolume= */ 100); + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder().setDeviceInfo(deviceInfo).build(); + remoteSession.setPlayer(playerConfig); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(deviceInfoFromParamRef.get()).isEqualTo(deviceInfo); + assertThat(deviceInfoFromGetterRef.get()).isEqualTo(deviceInfo); + } + + @Test + public void onDeviceInfoChanged_isCalledByDeviceInfoChange() throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + AtomicReference deviceInfoFromParamRef = new AtomicReference<>(); + AtomicReference deviceInfoFromGetterRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onDeviceInfoChanged(@NonNull DeviceInfo deviceInfo) { + deviceInfoFromParamRef.set(deviceInfo); + deviceInfoFromGetterRef.set(controller.getDeviceInfo()); + latch.countDown(); + } + }; + controller.addListener(callback); + + DeviceInfo deviceInfo = + new DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, /* minVolume= */ 1, /* maxVolume= */ 23); + remoteSession.getMockPlayer().notifyDeviceInfoChanged(deviceInfo); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(deviceInfoFromParamRef.get()).isEqualTo(deviceInfo); + assertThat(deviceInfoFromGetterRef.get()).isEqualTo(deviceInfo); + } + + @Test + public void onDeviceVolumeChanged_isCalledByDeviceVolumeChange() throws Exception { + DeviceInfo deviceInfo = + new DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, /* minVolume= */ 0, /* maxVolume= */ 100); + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setDeviceInfo(deviceInfo) + .setDeviceVolume(23) + .build(); + remoteSession.setPlayer(playerConfig); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + AtomicInteger deviceVolumeFromParamRef = new AtomicInteger(); + AtomicInteger deviceVolumeFromGetterRef = new AtomicInteger(); + CountDownLatch latch = new CountDownLatch(1); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onDeviceVolumeChanged(int volume, boolean muted) { + deviceVolumeFromParamRef.set(volume); + deviceVolumeFromGetterRef.set(controller.getDeviceVolume()); + latch.countDown(); + } + }; + controller.addListener(callback); + + int targetVolume = 45; + remoteSession.getMockPlayer().notifyDeviceVolumeChanged(targetVolume, /* muted= */ false); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(deviceVolumeFromParamRef.get()).isEqualTo(targetVolume); + assertThat(deviceVolumeFromGetterRef.get()).isEqualTo(targetVolume); + } + + @Test + public void onDeviceVolumeChanged_isCalledByDeviceMutedChange() throws Exception { + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder().setDeviceMuted(false).build(); + remoteSession.setPlayer(playerConfig); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + AtomicBoolean deviceMutedFromParamRef = new AtomicBoolean(); + AtomicBoolean deviceMutedFromGetterRef = new AtomicBoolean(); + CountDownLatch latch = new CountDownLatch(1); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onDeviceVolumeChanged(int volume, boolean muted) { + deviceMutedFromParamRef.set(muted); + deviceMutedFromGetterRef.set(controller.isDeviceMuted()); + latch.countDown(); + } + }; + controller.addListener(callback); + + remoteSession.getMockPlayer().notifyDeviceVolumeChanged(/* volume= */ 0, /* muted= */ true); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(deviceMutedFromParamRef.get()).isTrue(); + assertThat(deviceMutedFromGetterRef.get()).isTrue(); + } + + @Test + public void onEvents_whenOnRepeatModeChanges_isCalledAfterOtherCallbacks() throws Exception { + Player.Events testEvents = + new Player.Events(new ExoFlags.Builder().add(EVENT_REPEAT_MODE_CHANGED).build()); + CopyOnWriteArrayList callbackEventCodes = new CopyOnWriteArrayList<>(); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + CountDownLatch latch = new CountDownLatch(2); + AtomicReference eventsRef = new AtomicReference<>(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { + callbackEventCodes.add(EVENT_REPEAT_MODE_CHANGED); + latch.countDown(); + } + + @Override + public void onEvents(Player player, Player.Events events) { + callbackEventCodes.add(EVENT_ON_EVENTS); + eventsRef.set(events); + latch.countDown(); + } + }; + controller.addListener(callback); + remoteSession.getMockPlayer().setRepeatMode(REPEAT_MODE_ONE); + remoteSession.getMockPlayer().notifyRepeatModeChanged(); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + + assertThat(callbackEventCodes).containsExactly(EVENT_REPEAT_MODE_CHANGED, EVENT_ON_EVENTS); + assertThat(eventsRef.get()).isEqualTo(testEvents); + } + + // TODO(b/144387281): Move this into a separate test class for state masking. + @Test + public void setPlayWhenReady_stateMasking() throws Exception { + boolean testPlayWhenReady = true; + @Player.PlaybackSuppressionReason int testReason = Player.PLAYBACK_SUPPRESSION_REASON_NONE; + boolean testIsPlaying = true; + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setPlaybackState(Player.STATE_READY) + .setPlayWhenReady(false) + .setPlaybackSuppressionReason( + Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS) + .build(); + remoteSession.setPlayer(playerConfig); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + CountDownLatch latch = new CountDownLatch(4); + AtomicBoolean playWhenReadyRef = new AtomicBoolean(); + AtomicInteger playbackSuppressionReasonRef = new AtomicInteger(); + AtomicBoolean isPlayingRef = new AtomicBoolean(); + AtomicReference onEventsRef = new AtomicReference<>(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPlayWhenReadyChanged( + boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { + playWhenReadyRef.set(playWhenReady); + latch.countDown(); + } + + @Override + public void onPlaybackSuppressionReasonChanged(int playbackSuppressionReason) { + playbackSuppressionReasonRef.set(playbackSuppressionReason); + latch.countDown(); + } + + @Override + public void onIsPlayingChanged(boolean isPlaying) { + isPlayingRef.set(isPlaying); + latch.countDown(); + } + + @Override + public void onEvents(Player player, Player.Events events) { + onEventsRef.set(events); + latch.countDown(); + } + }; + controller.addListener(callback); + + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setPlayWhenReady(testPlayWhenReady); + assertThat(controller.getPlayWhenReady()).isEqualTo(testPlayWhenReady); + assertThat(controller.getPlaybackSuppressionReason()).isEqualTo(testReason); + assertThat(controller.isPlaying()).isEqualTo(testIsPlaying); + }); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(playWhenReadyRef.get()).isEqualTo(testPlayWhenReady); + assertThat(playbackSuppressionReasonRef.get()).isEqualTo(testReason); + assertThat(isPlayingRef.get()).isEqualTo(testIsPlaying); + assertThat(onEventsRef.get().contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)).isTrue(); + assertThat(onEventsRef.get().contains(Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED)) + .isTrue(); + assertThat(onEventsRef.get().contains(Player.EVENT_IS_PLAYING_CHANGED)).isTrue(); + } + + // TODO(b/144387281): Move this into a separate test class for state masking. + @Test + public void setShuffleModeEnabled_stateMasking() throws Exception { + boolean testShuffleModeEnabled = true; + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder().setShuffleModeEnabled(false).build(); + remoteSession.setPlayer(playerConfig); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + CountDownLatch latch = new CountDownLatch(2); + AtomicBoolean shuffleModeEnabledRef = new AtomicBoolean(); + AtomicReference onEventsRef = new AtomicReference<>(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + shuffleModeEnabledRef.set(shuffleModeEnabled); + latch.countDown(); + } + + @Override + public void onEvents(Player player, Player.Events events) { + onEventsRef.set(events); + latch.countDown(); + } + }; + controller.addListener(callback); + + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setShuffleModeEnabled(testShuffleModeEnabled); + assertThat(controller.getShuffleModeEnabled()).isEqualTo(testShuffleModeEnabled); + }); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(shuffleModeEnabledRef.get()).isEqualTo(testShuffleModeEnabled); + assertThat(onEventsRef.get().contains(Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED)).isTrue(); + } + + // TODO(b/144387281): Move this into a separate test class for state masking. + @Test + public void setRepeatMode_stateMasking() throws Exception { + int testRepeatMode = REPEAT_MODE_ALL; + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder().setRepeatMode(REPEAT_MODE_ONE).build(); + remoteSession.setPlayer(playerConfig); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + CountDownLatch latch = new CountDownLatch(2); + AtomicInteger repeatModeRef = new AtomicInteger(); + AtomicReference onEventsRef = new AtomicReference<>(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onRepeatModeChanged(int repeatMode) { + repeatModeRef.set(repeatMode); + latch.countDown(); + } + + @Override + public void onEvents(Player player, Player.Events events) { + onEventsRef.set(events); + latch.countDown(); + } + }; + controller.addListener(callback); + + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setRepeatMode(testRepeatMode); + assertThat(controller.getRepeatMode()).isEqualTo(testRepeatMode); + }); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(repeatModeRef.get()).isEqualTo(testRepeatMode); + assertThat(onEventsRef.get().contains(Player.EVENT_REPEAT_MODE_CHANGED)).isTrue(); + } + + private void testControllerAfterSessionIsClosed(@NonNull String id) throws Exception { + // This cause session service to be died. + remoteSession.release(); + controllerTestRule.waitForDisconnect(controller, true); + testNoInteraction(); + + // Ensure that the controller cannot use newly create session with the same ID. + // Recreated session has different session stub, so previously created controller + // shouldn't be available. + remoteSession = createRemoteMediaSession(id); + testNoInteraction(); + } + + // Test that session and controller doesn't interact. + // Note that this method can be called after the session is died, so session may not have + // valid player. + private void testNoInteraction() throws Exception { + // TODO: check that calls from the controller to session shouldn't be delivered. + + // Calls from the session to controller shouldn't be delivered. + CountDownLatch latch = new CountDownLatch(1); + controllerTestRule.setRunnableForOnCustomCommand( + controller, + new Runnable() { + @Override + public void run() { + latch.countDown(); + } + }); + SessionCommand customCommand = new SessionCommand("testNoInteraction", null); + + remoteSession.broadcastCustomCommand(customCommand, null); + + assertThat(latch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); + controllerTestRule.setRunnableForOnCustomCommand(controller, null); + } + + private RemoteMediaSession createRemoteMediaSession(@NonNull String id) throws RemoteException { + return new RemoteMediaSession(id, context, /* tokenExtras= */ null); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaControllerCallbackWithMediaSessionCompatTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaControllerCallbackWithMediaSessionCompatTest.java new file mode 100644 index 0000000000..9dfef301e3 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaControllerCallbackWithMediaSessionCompatTest.java @@ -0,0 +1,110 @@ +/* + * Copyright 2021 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.session; + +import static com.google.android.exoplayer2.Player.EVENT_REPEAT_MODE_CHANGED; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.DEFAULT_TEST_NAME; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.TIMEOUT_MS; +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.Context; +import android.os.RemoteException; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.session.vct.common.HandlerThreadTestRule; +import com.google.android.exoplayer2.session.vct.common.MainLooperTestRule; +import com.google.android.exoplayer2.util.ExoFlags; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; + +/** Tests for {@link MediaController.ControllerCallback} with {@link MediaSessionCompat}. */ +@RunWith(AndroidJUnit4.class) +@LargeTest +public class MediaControllerCallbackWithMediaSessionCompatTest { + + @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); + + private static final int EVENT_ON_EVENTS = C.INDEX_UNSET; + + private final HandlerThreadTestRule threadTestRule = + new HandlerThreadTestRule("MediaControllerCallbackTest"); + private final MediaControllerTestRule controllerTestRule = + new MediaControllerTestRule(threadTestRule); + + @Rule + public final TestRule chain = RuleChain.outerRule(threadTestRule).around(controllerTestRule); + + private Context context; + private RemoteMediaSessionCompat session; + + @Before + public void setUp() throws Exception { + context = ApplicationProvider.getApplicationContext(); + session = new RemoteMediaSessionCompat(DEFAULT_TEST_NAME, context); + } + + @After + public void cleanUp() throws RemoteException { + session.cleanUp(); + } + + @Test + public void onEvents_whenOnRepeatModeChanges_isCalledAfterOtherCallbacks() throws Exception { + Player.Events testEvents = + new Player.Events(new ExoFlags.Builder().add(EVENT_REPEAT_MODE_CHANGED).build()); + CopyOnWriteArrayList callbackEventCodes = new CopyOnWriteArrayList<>(); + + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch latch = new CountDownLatch(2); + AtomicReference eventsRef = new AtomicReference<>(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { + callbackEventCodes.add(EVENT_REPEAT_MODE_CHANGED); + latch.countDown(); + } + + @Override + public void onEvents(Player player, Player.Events events) { + callbackEventCodes.add(EVENT_ON_EVENTS); + eventsRef.set(events); + latch.countDown(); + } + }; + controller.addListener(callback); + session.setRepeatMode(PlaybackStateCompat.REPEAT_MODE_GROUP); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + + assertThat(callbackEventCodes).containsExactly(EVENT_REPEAT_MODE_CHANGED, EVENT_ON_EVENTS); + assertThat(eventsRef.get()).isEqualTo(testEvents); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaControllerCompatCallbackWithMediaSessionTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaControllerCompatCallbackWithMediaSessionTest.java new file mode 100644 index 0000000000..e22dee8b50 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaControllerCompatCallbackWithMediaSessionTest.java @@ -0,0 +1,947 @@ +/* + * 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.google.android.exoplayer2.session; + +import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_DURATION; +import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_MEDIA_ID; +import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_USER_RATING; +import static com.google.android.exoplayer2.Player.STATE_ENDED; +import static com.google.android.exoplayer2.Player.STATE_READY; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.LONG_TIMEOUT_MS; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.TIMEOUT_MS; +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.Context; +import android.media.AudioManager; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.RatingCompat; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.MediaSessionCompat.QueueItem; +import android.support.v4.media.session.PlaybackStateCompat; +import androidx.media.AudioAttributesCompat; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.HeartRating; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaMetadata; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.RepeatMode; +import com.google.android.exoplayer2.Player.State; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.audio.AudioAttributes; +import com.google.android.exoplayer2.device.DeviceInfo; +import com.google.android.exoplayer2.session.vct.common.HandlerThreadTestRule; +import com.google.android.exoplayer2.session.vct.common.PollingCheck; +import com.google.android.exoplayer2.session.vct.common.TestHandler; +import com.google.android.exoplayer2.session.vct.common.TestUtils; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link MediaControllerCompat.Callback} with {@link MediaSession}. */ +@RunWith(AndroidJUnit4.class) +@LargeTest +public class MediaControllerCompatCallbackWithMediaSessionTest { + + private static final String TAG = "MCCCallbackTestWithMS2"; + private static final float EPSILON = 1e-6f; + + @Rule public final HandlerThreadTestRule threadTestRule = new HandlerThreadTestRule(TAG); + + private Context context; + private TestHandler handler; + private RemoteMediaSession session; + private MediaControllerCompat controllerCompat; + + @Before + public void setUp() throws Exception { + context = ApplicationProvider.getApplicationContext(); + handler = threadTestRule.getHandler(); + session = new RemoteMediaSession(TAG, context, null); + controllerCompat = new MediaControllerCompat(context, session.getCompatToken()); + } + + @After + public void cleanUp() throws Exception { + session.release(); + } + + @Test + public void gettersAfterConnected() throws Exception { + @State int testState = STATE_READY; + int testBufferingPosition = 1500; + float testSpeed = 1.5f; + int testItemIndex = 0; + List testMediaItems = MediaTestUtils.createConvergedMediaItems(/* size= */ 3); + testMediaItems.set( + testItemIndex, + new MediaItem.Builder() + .setMediaId(testMediaItems.get(testItemIndex).mediaId) + .setMediaMetadata( + new MediaMetadata.Builder() + .setUserRating(new HeartRating(/* isHeart= */ true)) + .build()) + .build()); + Timeline testTimeline = new PlaylistTimeline(testMediaItems); + String testPlaylistTitle = "testPlaylistTitle"; + MediaMetadata testPlaylistMetadata = + new MediaMetadata.Builder().setTitle(testPlaylistTitle).build(); + boolean testShuffleModeEnabled = true; + @RepeatMode int testRepeatMode = Player.REPEAT_MODE_ONE; + + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setPlaybackState(testState) + .setBufferedPosition(testBufferingPosition) + .setPlaybackParameters(new PlaybackParameters(testSpeed)) + .setTimeline(testTimeline) + .setPlaylistMetadata(testPlaylistMetadata) + .setCurrentMediaItem(testMediaItems.get(testItemIndex)) + .setShuffleModeEnabled(testShuffleModeEnabled) + .setRepeatMode(testRepeatMode) + .build(); + session.setPlayer(playerConfig); + + MediaControllerCompat controller = new MediaControllerCompat(context, session.getCompatToken()); + CountDownLatch latch = new CountDownLatch(1); + controller.registerCallback( + new MediaControllerCompat.Callback() { + @Override + public void onSessionReady() { + latch.countDown(); + } + }, + handler); + + assertThat(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); + assertThat( + MediaUtils.convertToPlaybackState( + controller.getPlaybackState(), + controller.getMetadata(), + /* timeDiffMs= */ C.TIME_UNSET)) + .isEqualTo(testState); + assertThat(controller.getPlaybackState().getBufferedPosition()) + .isEqualTo(testBufferingPosition); + assertThat(controller.getPlaybackState().getPlaybackSpeed()).isWithin(EPSILON).of(testSpeed); + + assertThat(controller.getMetadata().getString(METADATA_KEY_MEDIA_ID)) + .isEqualTo(testMediaItems.get(testItemIndex).mediaId); + assertThat(controller.getRatingType()).isEqualTo(RatingCompat.RATING_HEART); + + List queue = controller.getQueue(); + assertThat(queue).isNotNull(); + assertThat(queue).hasSize(testTimeline.getWindowCount()); + for (int i = 0; i < testTimeline.getWindowCount(); i++) { + assertThat(queue.get(i).getDescription().getMediaId()) + .isEqualTo(testMediaItems.get(i).mediaId); + } + assertThat(testPlaylistTitle).isEqualTo(controller.getQueueTitle().toString()); + assertThat(PlaybackStateCompat.SHUFFLE_MODE_ALL).isEqualTo(controller.getShuffleMode()); + assertThat(PlaybackStateCompat.REPEAT_MODE_ONE).isEqualTo(controller.getRepeatMode()); + } + + @Test + public void getError_withPlayerErrorAfterConnected_returnsError() throws Exception { + ExoPlaybackException testPlayerError = ExoPlaybackException.createForRemote("testremote"); + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder().setPlayerError(testPlayerError).build(); + session.setPlayer(playerConfig); + + MediaControllerCompat controller = new MediaControllerCompat(context, session.getCompatToken()); + CountDownLatch latch = new CountDownLatch(1); + controller.registerCallback( + new MediaControllerCompat.Callback() { + @Override + public void onSessionReady() { + latch.countDown(); + } + }, + handler); + + assertThat(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); + assertPlaybackStateCompatErrorEquals(controller.getPlaybackState(), testPlayerError); + } + + @Test + public void playerError_notified() throws Exception { + ExoPlaybackException playerError = + ExoPlaybackException.createForUnexpected(new RuntimeException("player error")); + + CountDownLatch latch = new CountDownLatch(1); + AtomicReference playbackStateCompatRef = new AtomicReference<>(); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackStateCompat state) { + playbackStateCompatRef.set(state); + latch.countDown(); + } + }; + controllerCompat.registerCallback(callback, handler); + + session.getMockPlayer().notifyPlayerError(playerError); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + PlaybackStateCompat state = playbackStateCompatRef.get(); + assertPlaybackStateCompatErrorEquals(state, playerError); + } + + @Test + public void repeatModeChange() throws Exception { + @PlaybackStateCompat.RepeatMode int testRepeatMode = PlaybackStateCompat.REPEAT_MODE_ALL; + + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger repeatModeRef = new AtomicInteger(); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onRepeatModeChanged(@PlaybackStateCompat.RepeatMode int repeatMode) { + repeatModeRef.set(repeatMode); + latch.countDown(); + } + }; + controllerCompat.registerCallback(callback, handler); + + session.getMockPlayer().setRepeatMode(Player.REPEAT_MODE_ALL); + session.getMockPlayer().notifyRepeatModeChanged(); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(repeatModeRef.get()).isEqualTo(testRepeatMode); + assertThat(controllerCompat.getRepeatMode()).isEqualTo(testRepeatMode); + } + + @Test + public void shuffleModeChange() throws Exception { + @PlaybackStateCompat.ShuffleMode + int testShuffleModeEnabled = PlaybackStateCompat.SHUFFLE_MODE_ALL; + + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger shuffleModeRef = new AtomicInteger(); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onShuffleModeChanged(@PlaybackStateCompat.ShuffleMode int shuffleMode) { + shuffleModeRef.set(shuffleMode); + latch.countDown(); + } + }; + controllerCompat.registerCallback(callback, handler); + + session.getMockPlayer().setShuffleModeEnabled(/* shuffleModeEnabled= */ true); + session.getMockPlayer().notifyShuffleModeEnabledChanged(); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(shuffleModeRef.get()).isEqualTo(testShuffleModeEnabled); + assertThat(controllerCompat.getShuffleMode()).isEqualTo(testShuffleModeEnabled); + } + + @Test + public void release() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onSessionDestroyed() { + latch.countDown(); + } + }; + controllerCompat.registerCallback(callback, handler); + + session.release(); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void setPlayer_isNotified() throws Exception { + @State int testState = STATE_READY; + boolean testPlayWhenReady = true; + long testDurationMs = 200; + long testCurrentPositionMs = 11; + long testBufferedPositionMs = 100; + PlaybackParameters playbackParameters = new PlaybackParameters(/* speed= */ 1.5f); + int testItemIndex = 0; + List testMediaItems = MediaTestUtils.createConvergedMediaItems(/* size= */ 3); + testMediaItems.set( + testItemIndex, + new MediaItem.Builder() + .setMediaId(testMediaItems.get(testItemIndex).mediaId) + .setMediaMetadata( + new MediaMetadata.Builder() + .setUserRating(new HeartRating(/* isHeart= */ true)) + .build()) + .build()); + Timeline testTimeline = new PlaylistTimeline(testMediaItems); + String testPlaylistTitle = "testPlaylistTitle"; + MediaMetadata testPlaylistMetadata = + new MediaMetadata.Builder().setTitle(testPlaylistTitle).build(); + boolean testShuffleModeEnabled = true; + @RepeatMode int testRepeatMode = Player.REPEAT_MODE_ONE; + + AtomicReference playbackStateRef = new AtomicReference<>(); + AtomicReference metadataRef = new AtomicReference<>(); + AtomicReference queueTitleRef = new AtomicReference<>(); + AtomicInteger shuffleModeRef = new AtomicInteger(); + AtomicInteger repeatModeRef = new AtomicInteger(); + CountDownLatch latchForPlaybackState = new CountDownLatch(1); + CountDownLatch latchForMetadata = new CountDownLatch(1); + CountDownLatch latchForQueue = new CountDownLatch(2); + CountDownLatch latchForShuffleMode = new CountDownLatch(1); + CountDownLatch latchForRepeatMode = new CountDownLatch(1); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackStateCompat state) { + playbackStateRef.set(state); + latchForPlaybackState.countDown(); + } + + @Override + public void onMetadataChanged(MediaMetadataCompat metadata) { + metadataRef.set(metadata); + latchForMetadata.countDown(); + } + + @Override + public void onQueueChanged(List queue) { + latchForQueue.countDown(); + } + + @Override + public void onQueueTitleChanged(CharSequence title) { + queueTitleRef.set(title); + latchForQueue.countDown(); + } + + @Override + public void onRepeatModeChanged(int repeatMode) { + repeatModeRef.set(repeatMode); + latchForRepeatMode.countDown(); + } + + @Override + public void onShuffleModeChanged(int shuffleMode) { + shuffleModeRef.set(shuffleMode); + latchForShuffleMode.countDown(); + } + }; + controllerCompat.registerCallback(callback, handler); + + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setPlaybackState(testState) + .setPlayWhenReady(testPlayWhenReady) + .setCurrentPosition(testCurrentPositionMs) + .setBufferedPosition(testBufferedPositionMs) + .setDuration(testDurationMs) + .setPlaybackParameters(playbackParameters) + .setTimeline(testTimeline) + .setPlaylistMetadata(testPlaylistMetadata) + .setCurrentMediaItem(testMediaItems.get(testItemIndex)) + .setShuffleModeEnabled(testShuffleModeEnabled) + .setRepeatMode(testRepeatMode) + .build(); + session.setPlayer(playerConfig); + + assertThat(latchForPlaybackState.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(playbackStateRef.get().getBufferedPosition()).isEqualTo(testBufferedPositionMs); + assertThat(playbackStateRef.get().getPosition()).isEqualTo(testCurrentPositionMs); + assertThat(playbackStateRef.get().getPlaybackSpeed()) + .isWithin(EPSILON) + .of(playbackParameters.speed); + + assertThat(latchForMetadata.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(metadataRef.get().getString(METADATA_KEY_MEDIA_ID)) + .isEqualTo(testMediaItems.get(testItemIndex).mediaId); + assertThat(metadataRef.get().getLong(METADATA_KEY_DURATION)).isEqualTo(testDurationMs); + @PlaybackStateCompat.State + int playbackStateFromControllerCompat = + MediaUtils.convertToPlaybackState( + playbackStateRef.get(), metadataRef.get(), /* timeDiffMs= */ C.TIME_UNSET); + assertThat(playbackStateFromControllerCompat).isEqualTo(testState); + assertThat(metadataRef.get().getRating(METADATA_KEY_USER_RATING).hasHeart()).isTrue(); + + assertThat(latchForQueue.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + List queue = controllerCompat.getQueue(); + assertThat(queue).hasSize(testTimeline.getWindowCount()); + for (int i = 0; i < testTimeline.getWindowCount(); i++) { + assertThat(queue.get(i).getDescription().getMediaId()) + .isEqualTo(testMediaItems.get(i).mediaId); + } + assertThat(queueTitleRef.get().toString()).isEqualTo(testPlaylistTitle); + + assertThat(latchForShuffleMode.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); + assertThat(shuffleModeRef.get()).isEqualTo(PlaybackStateCompat.SHUFFLE_MODE_ALL); + assertThat(latchForRepeatMode.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); + assertThat(repeatModeRef.get()).isEqualTo(PlaybackStateCompat.REPEAT_MODE_ONE); + } + + @Test + public void setPlayer_playbackTypeChangedToRemote() throws Exception { + DeviceInfo deviceInfo = + new DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, /* minVolume= */ 0, /* maxVolume= */ 25); + int legacyPlaybackType = MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE; + int deviceVolume = 10; + + CountDownLatch playbackInfoNotified = new CountDownLatch(1); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onAudioInfoChanged(MediaControllerCompat.PlaybackInfo info) { + if (info.getPlaybackType() == legacyPlaybackType + && info.getMaxVolume() == deviceInfo.maxVolume + && info.getCurrentVolume() == deviceVolume) { + playbackInfoNotified.countDown(); + } + } + }; + controllerCompat.registerCallback(callback, handler); + + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setDeviceInfo(deviceInfo) + .setDeviceVolume(deviceVolume) + .build(); + session.setPlayer(playerConfig); + + assertThat(playbackInfoNotified.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + MediaControllerCompat.PlaybackInfo info = controllerCompat.getPlaybackInfo(); + assertThat(info.getPlaybackType()).isEqualTo(legacyPlaybackType); + assertThat(info.getMaxVolume()).isEqualTo(deviceInfo.maxVolume); + assertThat(info.getCurrentVolume()).isEqualTo(deviceVolume); + } + + @Test + public void setPlayer_playbackTypeChangedToLocal() throws Exception { + DeviceInfo deviceInfo = + new DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, /* minVolume= */ 0, /* maxVolume= */ 10); + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder().setDeviceInfo(deviceInfo).build(); + session.setPlayer(playerConfig); + + DeviceInfo deviceInfoToUpdate = + new DeviceInfo(DeviceInfo.PLAYBACK_TYPE_LOCAL, /* minVolume= */ 0, /* maxVolume= */ 10); + int legacyPlaybackTypeToUpdate = MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL; + int legacyStream = AudioManager.STREAM_RING; + AudioAttributesCompat attrsCompat = + new AudioAttributesCompat.Builder().setLegacyStreamType(legacyStream).build(); + AudioAttributes attrs = MediaUtils.convertToAudioAttributes(attrsCompat); + + CountDownLatch playbackInfoNotified = new CountDownLatch(1); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onAudioInfoChanged(MediaControllerCompat.PlaybackInfo info) { + if (info.getPlaybackType() == legacyPlaybackTypeToUpdate + && info.getAudioAttributes().getLegacyStreamType() == legacyStream) { + playbackInfoNotified.countDown(); + } + } + }; + controllerCompat.registerCallback(callback, handler); + + Bundle playerConfigToUpdate = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setDeviceInfo(deviceInfoToUpdate) + .setAudioAttributes(attrs) + .build(); + session.setPlayer(playerConfigToUpdate); + + // In API 21 and 22, onAudioInfoChanged is not called when playback is changed to local. + if (Build.VERSION.SDK_INT == 21 || Build.VERSION.SDK_INT == 22) { + PollingCheck.waitFor( + TIMEOUT_MS, + () -> { + MediaControllerCompat.PlaybackInfo info = controllerCompat.getPlaybackInfo(); + return info.getPlaybackType() == legacyPlaybackTypeToUpdate + && info.getAudioAttributes().getLegacyStreamType() == legacyStream; + }); + } else { + assertThat(playbackInfoNotified.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + MediaControllerCompat.PlaybackInfo info = controllerCompat.getPlaybackInfo(); + assertThat(info.getPlaybackType()).isEqualTo(legacyPlaybackTypeToUpdate); + assertThat(info.getAudioAttributes().getLegacyStreamType()).isEqualTo(legacyStream); + } + } + + @Test + public void setPlayer_playbackTypeNotChanged_local() throws Exception { + DeviceInfo deviceInfo = + new DeviceInfo(DeviceInfo.PLAYBACK_TYPE_LOCAL, /* minVolume= */ 0, /* maxVolume= */ 10); + int legacyPlaybackType = MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL; + int legacyStream = AudioManager.STREAM_RING; + AudioAttributesCompat attrsCompat = + new AudioAttributesCompat.Builder().setLegacyStreamType(legacyStream).build(); + AudioAttributes attrs = MediaUtils.convertToAudioAttributes(attrsCompat); + + CountDownLatch playbackInfoNotified = new CountDownLatch(1); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onAudioInfoChanged(MediaControllerCompat.PlaybackInfo info) { + if (info.getPlaybackType() == legacyPlaybackType + && info.getAudioAttributes().getLegacyStreamType() == legacyStream) { + playbackInfoNotified.countDown(); + } + } + }; + controllerCompat.registerCallback(callback, handler); + + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setDeviceInfo(deviceInfo) + .setAudioAttributes(attrs) + .build(); + session.setPlayer(playerConfig); + + // In API 21+, onAudioInfoChanged() is not called when playbackType is not changed. + if (Build.VERSION.SDK_INT >= 21) { + PollingCheck.waitFor( + TIMEOUT_MS, + () -> { + MediaControllerCompat.PlaybackInfo info = controllerCompat.getPlaybackInfo(); + return info.getPlaybackType() == legacyPlaybackType + && info.getAudioAttributes().getLegacyStreamType() == legacyStream; + }); + } else { + assertThat(playbackInfoNotified.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + MediaControllerCompat.PlaybackInfo info = controllerCompat.getPlaybackInfo(); + assertThat(info.getPlaybackType()).isEqualTo(legacyPlaybackType); + assertThat(info.getAudioAttributes().getLegacyStreamType()).isEqualTo(legacyStream); + } + } + + @Test + public void setPlayer_playbackTypeNotChanged_remote() throws Exception { + DeviceInfo deviceInfo = + new DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, /* minVolume= */ 0, /* maxVolume= */ 10); + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setDeviceInfo(deviceInfo) + .setDeviceVolume(1) + .build(); + session.setPlayer(playerConfig); + + DeviceInfo deviceInfoToUpdate = + new DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, /* minVolume= */ 0, /* maxVolume= */ 25); + int legacyPlaybackTypeToUpdate = MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE; + int deviceVolumeToUpdate = 10; + + CountDownLatch playbackInfoNotified = new CountDownLatch(1); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onAudioInfoChanged(MediaControllerCompat.PlaybackInfo info) { + if (info.getPlaybackType() == legacyPlaybackTypeToUpdate + && info.getMaxVolume() == deviceInfoToUpdate.maxVolume + && info.getCurrentVolume() == deviceVolumeToUpdate) { + playbackInfoNotified.countDown(); + } + } + }; + controllerCompat.registerCallback(callback, handler); + + Bundle playerConfigToUpdate = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setDeviceInfo(deviceInfoToUpdate) + .setDeviceVolume(deviceVolumeToUpdate) + .build(); + session.setPlayer(playerConfigToUpdate); + + // In API 21+, onAudioInfoChanged() is not called when playbackType is not changed. + if (Build.VERSION.SDK_INT >= 21) { + PollingCheck.waitFor( + TIMEOUT_MS, + () -> { + MediaControllerCompat.PlaybackInfo info = controllerCompat.getPlaybackInfo(); + return info.getPlaybackType() == legacyPlaybackTypeToUpdate + && info.getMaxVolume() == deviceInfoToUpdate.maxVolume + && info.getCurrentVolume() == deviceVolumeToUpdate; + }); + } else { + assertThat(playbackInfoNotified.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + MediaControllerCompat.PlaybackInfo info = controllerCompat.getPlaybackInfo(); + assertThat(info.getPlaybackType()).isEqualTo(legacyPlaybackTypeToUpdate); + assertThat(info.getMaxVolume()).isEqualTo(deviceInfoToUpdate.maxVolume); + assertThat(info.getCurrentVolume()).isEqualTo(deviceVolumeToUpdate); + } + } + + @Test + public void onPlaybackParametersChanged_notifiesPlaybackStateCompatChanges() throws Exception { + PlaybackParameters playbackParameters = new PlaybackParameters(/* speed= */ 1.5f); + + AtomicReference playbackStateRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackStateCompat state) { + playbackStateRef.set(state); + latch.countDown(); + } + }; + controllerCompat.registerCallback(callback, handler); + + session.getMockPlayer().notifyPlaybackParametersChanged(playbackParameters); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(playbackStateRef.get().getPlaybackSpeed()) + .isWithin(EPSILON) + .of(playbackParameters.speed); + assertThat(controllerCompat.getPlaybackState().getPlaybackSpeed()) + .isWithin(EPSILON) + .of(playbackParameters.speed); + } + + @Test + public void playbackStateChange_playWhenReadyBecomesFalseWhenReady_notifiesPaused() + throws Exception { + session + .getMockPlayer() + .setPlayWhenReady( + /* playWhenReady= */ true, + Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS); + session.getMockPlayer().notifyPlaybackStateChanged(STATE_READY); + session.getMockPlayer().notifyIsPlayingChanged(false); + + AtomicReference playbackStateCompatRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackStateCompat playbackStateCompat) { + playbackStateCompatRef.set(playbackStateCompat); + latch.countDown(); + } + }; + controllerCompat.registerCallback(callback, handler); + + session + .getMockPlayer() + .notifyPlayWhenReadyChanged( + /* playWhenReady= */ false, Player.PLAYBACK_SUPPRESSION_REASON_NONE); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + + assertThat(playbackStateCompatRef.get().getState()).isEqualTo(PlaybackStateCompat.STATE_PAUSED); + } + + @Test + public void playbackStateChange_playWhenReadyBecomesTrueWhenBuffering_notifiesBuffering() + throws Exception { + session + .getMockPlayer() + .setPlayWhenReady(/* playWhenReady= */ false, Player.PLAYBACK_SUPPRESSION_REASON_NONE); + session.getMockPlayer().notifyPlaybackStateChanged(Player.STATE_BUFFERING); + session.getMockPlayer().notifyIsPlayingChanged(false); + + AtomicReference playbackStateCompatRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackStateCompat playbackStateCompat) { + playbackStateCompatRef.set(playbackStateCompat); + latch.countDown(); + } + }; + controllerCompat.registerCallback(callback, handler); + session + .getMockPlayer() + .notifyPlayWhenReadyChanged( + /* playWhenReady= */ true, Player.PLAYBACK_SUPPRESSION_REASON_NONE); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + + assertThat(playbackStateCompatRef.get().getState()) + .isEqualTo(PlaybackStateCompat.STATE_BUFFERING); + } + + @Test + public void playbackStateChange_playbackStateBecomesEnded_notifiesPaused() throws Exception { + session + .getMockPlayer() + .setPlayWhenReady( + /* playWhenReady= */ true, + Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS); + session.getMockPlayer().notifyPlaybackStateChanged(STATE_READY); + session.getMockPlayer().notifyIsPlayingChanged(false); + + AtomicReference playbackStateCompatRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackStateCompat playbackStateCompat) { + playbackStateCompatRef.set(playbackStateCompat); + latch.countDown(); + } + }; + controllerCompat.registerCallback(callback, handler); + + session.getMockPlayer().notifyPlaybackStateChanged(STATE_ENDED); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + + assertThat(playbackStateCompatRef.get().getState()).isEqualTo(PlaybackStateCompat.STATE_PAUSED); + } + + @Test + public void playbackStateChange_isPlayingBecomesTrue_notifiesPlaying() throws Exception { + session.getMockPlayer().notifyIsPlayingChanged(false); + + AtomicReference playbackStateCompatRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackStateCompat playbackStateCompat) { + playbackStateCompatRef.set(playbackStateCompat); + latch.countDown(); + } + }; + controllerCompat.registerCallback(callback, handler); + + session.getMockPlayer().notifyIsPlayingChanged(true); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + + assertThat(playbackStateCompatRef.get().getState()) + .isEqualTo(PlaybackStateCompat.STATE_PLAYING); + } + + @Test + public void playbackStateChange_positionDiscontinuityNotifies_updatesPosition() throws Exception { + long testSeekPosition = 1300; + + AtomicReference playbackStateRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackStateCompat state) { + playbackStateRef.set(state); + latch.countDown(); + } + }; + controllerCompat.registerCallback(callback, handler); + + session.getMockPlayer().setCurrentPosition(testSeekPosition); + session + .getMockPlayer() + .notifyPositionDiscontinuity( + /* oldPosition= */ SessionPositionInfo.DEFAULT_POSITION_INFO, + /* newPosition= */ SessionPositionInfo.DEFAULT_POSITION_INFO, + Player.DISCONTINUITY_REASON_SEEK); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(playbackStateRef.get().getPosition()).isEqualTo(testSeekPosition); + assertThat(controllerCompat.getPlaybackState().getPosition()).isEqualTo(testSeekPosition); + } + + @Test + public void currentMediaItemChange() throws Exception { + int testItemIndex = 3; + long testPosition = 1234; + String testDisplayTitle = "displayTitle"; + List testMediaItems = MediaTestUtils.createConvergedMediaItems(/* size= */ 5); + testMediaItems.set( + testItemIndex, + new MediaItem.Builder() + .setMediaId(testMediaItems.get(testItemIndex).mediaId) + .setMediaMetadata(new MediaMetadata.Builder().setTitle(testDisplayTitle).build()) + .build()); + Timeline timeline = new PlaylistTimeline(testMediaItems); + session.getMockPlayer().setTimeline(timeline); + + AtomicReference metadataRef = new AtomicReference<>(); + AtomicReference playbackStateRef = new AtomicReference<>(); + CountDownLatch latchForMetadata = new CountDownLatch(1); + CountDownLatch latchForPlaybackState = new CountDownLatch(1); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onMetadataChanged(MediaMetadataCompat metadata) { + metadataRef.set(metadata); + latchForMetadata.countDown(); + } + + @Override + public void onPlaybackStateChanged(PlaybackStateCompat state) { + playbackStateRef.set(state); + latchForPlaybackState.countDown(); + } + }; + controllerCompat.registerCallback(callback, handler); + + session.getMockPlayer().setCurrentWindowIndex(testItemIndex); + session.getMockPlayer().setCurrentPosition(testPosition); + session + .getMockPlayer() + .notifyMediaItemTransition(testItemIndex, Player.MEDIA_ITEM_TRANSITION_REASON_SEEK); + + assertThat(latchForMetadata.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(metadataRef.get().getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE)) + .isEqualTo(testDisplayTitle); + assertThat( + controllerCompat + .getMetadata() + .getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE)) + .isEqualTo(testDisplayTitle); + assertThat(latchForPlaybackState.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(playbackStateRef.get().getPosition()).isEqualTo(testPosition); + assertThat(controllerCompat.getPlaybackState().getPosition()).isEqualTo(testPosition); + assertThat(playbackStateRef.get().getActiveQueueItemId()) + .isEqualTo(MediaUtils.convertToQueueItemId(testItemIndex)); + assertThat(controllerCompat.getPlaybackState().getActiveQueueItemId()) + .isEqualTo(MediaUtils.convertToQueueItemId(testItemIndex)); + } + + @Test + public void playlistChange() throws Exception { + AtomicReference> queueRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onQueueChanged(List queue) { + queueRef.set(queue); + latch.countDown(); + } + }; + controllerCompat.registerCallback(callback, handler); + + Timeline timeline = MediaTestUtils.createTimeline(/* windowCount= */ 5); + session.getMockPlayer().setTimeline(timeline); + session.getMockPlayer().notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + List queueFromParam = queueRef.get(); + List queueFromGetter = controllerCompat.getQueue(); + assertThat(queueFromParam).hasSize(timeline.getWindowCount()); + assertThat(queueFromGetter).hasSize(timeline.getWindowCount()); + Timeline.Window window = new Timeline.Window(); + for (int i = 0; i < timeline.getWindowCount(); i++) { + assertThat(queueFromParam.get(i).getDescription().getMediaId()) + .isEqualTo(timeline.getWindow(i, window).mediaItem.mediaId); + assertThat(queueFromGetter.get(i).getDescription().getMediaId()) + .isEqualTo(timeline.getWindow(i, window).mediaItem.mediaId); + } + } + + @Test + public void playlistChange_longList() throws Exception { + AtomicReference> queueRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onQueueChanged(List queue) { + queueRef.set(queue); + latch.countDown(); + } + }; + controllerCompat.registerCallback(callback, handler); + + int listSize = 5_000; + session.getMockPlayer().createAndSetFakeTimeline(listSize); + session.getMockPlayer().notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + + assertThat(latch.await(LONG_TIMEOUT_MS, MILLISECONDS)).isTrue(); + List queueFromParam = queueRef.get(); + List queueFromGetter = controllerCompat.getQueue(); + if (Build.VERSION.SDK_INT >= 21) { + assertThat(queueFromParam).hasSize(listSize); + assertThat(queueFromGetter).hasSize(listSize); + } else { + // Below API 21, only the initial part of the playlist is sent to the + // MediaControllerCompat when the list is too long. + assertThat(queueFromParam.size() < listSize).isTrue(); + assertThat(queueFromGetter).hasSize(queueFromParam.size()); + } + for (int i = 0; i < listSize; i++) { + assertThat(queueFromParam.get(i).getDescription().getMediaId()) + .isEqualTo(TestUtils.getMediaIdInFakeTimeline(i)); + assertThat(queueFromGetter.get(i).getDescription().getMediaId()) + .isEqualTo(TestUtils.getMediaIdInFakeTimeline(i)); + } + } + + @Test + public void playlistMetadataChange() throws Exception { + AtomicReference queueTitleRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onQueueTitleChanged(CharSequence title) { + queueTitleRef.set(title); + latch.countDown(); + } + }; + controllerCompat.registerCallback(callback, handler); + + String playlistTitle = "playlistTitle"; + MediaMetadata playlistMetadata = new MediaMetadata.Builder().setTitle(playlistTitle).build(); + session.getMockPlayer().setPlaylistMetadata(playlistMetadata); + session.getMockPlayer().notifyPlaylistMetadataChanged(); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(queueTitleRef.get().toString()).isEqualTo(playlistTitle); + } + + @Test + public void onAudioInfoChanged_isCalledByVolumeChange() throws Exception { + DeviceInfo deviceInfo = + new DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, /* minVolume= */ 0, /* maxVolume= */ 10); + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setDeviceInfo(deviceInfo) + .setDeviceVolume(1) + .build(); + session.setPlayer(playerConfig); + + int targetVolume = 3; + CountDownLatch targetVolumeNotified = new CountDownLatch(1); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onAudioInfoChanged(MediaControllerCompat.PlaybackInfo info) { + if (info.getCurrentVolume() == targetVolume) { + targetVolumeNotified.countDown(); + } + } + }; + controllerCompat.registerCallback(callback, handler); + + session.getMockPlayer().notifyDeviceVolumeChanged(targetVolume, /* muted= */ false); + + assertThat(targetVolumeNotified.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(controllerCompat.getPlaybackInfo().getCurrentVolume()).isEqualTo(targetVolume); + } + + private static void assertPlaybackStateCompatErrorEquals( + PlaybackStateCompat state, ExoPlaybackException playerError) { + assertThat(state.getState()).isEqualTo(PlaybackStateCompat.STATE_ERROR); + assertThat(state.getErrorCode()).isEqualTo(PlaybackStateCompat.ERROR_CODE_UNKNOWN_ERROR); + assertThat(state.getErrorMessage()).isEqualTo(playerError.getMessage()); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaControllerSurfaceTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaControllerSurfaceTest.java new file mode 100644 index 0000000000..e577354d25 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaControllerSurfaceTest.java @@ -0,0 +1,137 @@ +/* + * Copyright 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. + */ + +package com.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.DEFAULT_TEST_NAME; +import static com.google.common.truth.Truth.assertThat; + +import android.os.RemoteException; +import android.view.Surface; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import androidx.test.rule.ActivityTestRule; +import com.google.android.exoplayer2.session.vct.common.HandlerThreadTestRule; +import com.google.android.exoplayer2.session.vct.common.SurfaceActivity; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; + +/** Tests for {@link MediaController#setVideoSurface(Surface)}. */ +@RunWith(AndroidJUnit4.class) +@LargeTest +public class MediaControllerSurfaceTest { + private static final String TAG = "MC_SurfaceTest"; + + private final HandlerThreadTestRule threadTestRule = new HandlerThreadTestRule(TAG); + private final MediaControllerTestRule controllerTestRule = + new MediaControllerTestRule(threadTestRule); + + @Rule + public final TestRule chain = RuleChain.outerRule(threadTestRule).around(controllerTestRule); + + private SurfaceActivity activity; + private RemoteMediaSession remoteSession; + + @Rule + public ActivityTestRule activityRule = + new ActivityTestRule<>(SurfaceActivity.class); + + @Before + public void setUp() throws Exception { + activity = activityRule.getActivity(); + + remoteSession = + new RemoteMediaSession( + DEFAULT_TEST_NAME, ApplicationProvider.getApplicationContext(), null); + } + + @After + public void cleanUp() throws RemoteException { + remoteSession.cleanUp(); + } + + @Test + public void setVideoSurface() throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + Surface testSurface = activity.getFirstSurfaceHolder().getSurface(); + + threadTestRule.getHandler().postAndSync(() -> controller.setVideoSurface(testSurface)); + + assertThat(remoteSession.getMockPlayer().surfaceExists()).isTrue(); + } + + @Test + public void setVideoSurface_withNull_clearsSurface() throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + Surface testSurface = activity.getFirstSurfaceHolder().getSurface(); + threadTestRule.getHandler().postAndSync(() -> controller.setVideoSurface(testSurface)); + + threadTestRule.getHandler().postAndSync(() -> controller.setVideoSurface(null)); + + assertThat(remoteSession.getMockPlayer().surfaceExists()).isFalse(); + } + + @Test + public void clearVideoSurface() throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + Surface testSurface = activity.getFirstSurfaceHolder().getSurface(); + threadTestRule.getHandler().postAndSync(() -> controller.setVideoSurface(testSurface)); + + threadTestRule.getHandler().postAndSync(() -> controller.clearVideoSurface()); + + assertThat(remoteSession.getMockPlayer().surfaceExists()).isFalse(); + } + + @Test + public void clearVideoSurface_withTheSameSurface() throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + Surface testSurface = activity.getFirstSurfaceHolder().getSurface(); + threadTestRule.getHandler().postAndSync(() -> controller.setVideoSurface(testSurface)); + + threadTestRule.getHandler().postAndSync(() -> controller.clearVideoSurface(testSurface)); + + assertThat(remoteSession.getMockPlayer().surfaceExists()).isFalse(); + } + + @Test + public void clearVideoSurface_withDifferentSurface_doesNothing() throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + Surface testSurface = activity.getFirstSurfaceHolder().getSurface(); + threadTestRule.getHandler().postAndSync(() -> controller.setVideoSurface(testSurface)); + Surface anotherSurface = activity.getSecondSurfaceHolder().getSurface(); + + threadTestRule.getHandler().postAndSync(() -> controller.clearVideoSurface(anotherSurface)); + + assertThat(remoteSession.getMockPlayer().surfaceExists()).isTrue(); + } + + @Test + public void clearVideoSurface_withNull_doesNothing() throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + Surface testSurface = activity.getFirstSurfaceHolder().getSurface(); + threadTestRule.getHandler().postAndSync(() -> controller.setVideoSurface(testSurface)); + + threadTestRule.getHandler().postAndSync(() -> controller.clearVideoSurface(null)); + + assertThat(remoteSession.getMockPlayer().surfaceExists()).isTrue(); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaControllerTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaControllerTest.java new file mode 100644 index 0000000000..ebd4d52bba --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaControllerTest.java @@ -0,0 +1,805 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.DEFAULT_TEST_NAME; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; +import static com.google.android.exoplayer2.session.vct.common.MediaSessionConstants.TEST_GET_SESSION_ACTIVITY; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.LONG_TIMEOUT_MS; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.TIMEOUT_MS; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.app.PendingIntent; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.os.RemoteException; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.HeartRating; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaMetadata; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.RepeatMode; +import com.google.android.exoplayer2.Rating; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.audio.AudioAttributes; +import com.google.android.exoplayer2.session.MediaController.ControllerCallback; +import com.google.android.exoplayer2.session.vct.common.HandlerThreadTestRule; +import com.google.android.exoplayer2.session.vct.common.MainLooperTestRule; +import com.google.android.exoplayer2.session.vct.common.PollingCheck; +import com.google.android.exoplayer2.session.vct.common.TestUtils; +import com.google.android.exoplayer2.video.VideoSize; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; + +/** Tests for {@link MediaController}. */ +@RunWith(AndroidJUnit4.class) +@LargeTest +public class MediaControllerTest { + + private static final String TAG = "MediaControllerTest"; + + @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); + + private final HandlerThreadTestRule threadTestRule = new HandlerThreadTestRule(TAG); + final MediaControllerTestRule controllerTestRule = new MediaControllerTestRule(threadTestRule); + + @Rule + public final TestRule chain = RuleChain.outerRule(threadTestRule).around(controllerTestRule); + + private final List remoteSessionList = new ArrayList<>(); + + private Context context; + private RemoteMediaSession remoteSession; + + @Before + public void setUp() throws Exception { + context = ApplicationProvider.getApplicationContext(); + remoteSession = createRemoteMediaSession(DEFAULT_TEST_NAME, null); + } + + @After + public void cleanUp() throws RemoteException { + for (int i = 0; i < remoteSessionList.size(); i++) { + RemoteMediaSession session = remoteSessionList.get(i); + if (session != null) { + session.cleanUp(); + } + } + } + + @Test + public void builder() throws Exception { + MediaController.Builder builder; + + try { + builder = new MediaController.Builder(null); + assertWithMessage("null context shouldn't be allowed").fail(); + } catch (NullPointerException e) { + // expected. pass-through + } + + try { + builder = new MediaController.Builder(context); + builder.setSessionToken(null); + assertWithMessage("null token shouldn't be allowed").fail(); + } catch (NullPointerException e) { + // expected. pass-through + } + + try { + builder = new MediaController.Builder(context); + builder.setSessionCompatToken(null); + assertWithMessage("null compat token shouldn't be allowed").fail(); + } catch (NullPointerException e) { + // expected. pass-through + } + + try { + builder = new MediaController.Builder(context); + builder.setControllerCallback(null); + assertWithMessage("null callback shouldn't be allowed").fail(); + } catch (NullPointerException e) { + // expected. pass-through + } + + try { + builder = new MediaController.Builder(context); + builder.setApplicationLooper(null); + assertWithMessage("null looper shouldn't be allowed").fail(); + } catch (NullPointerException e) { + // expected. pass-through + } + + MediaController controller = + new MediaController.Builder(context) + .setSessionToken(remoteSession.getToken()) + .setControllerCallback(new ControllerCallback() {}) + .setApplicationLooper(threadTestRule.getHandler().getLooper()) + .build(); + threadTestRule.getHandler().postAndSync(controller::release); + } + + @Test + public void getSessionActivity() throws Exception { + RemoteMediaSession session = createRemoteMediaSession(TEST_GET_SESSION_ACTIVITY, null); + + MediaController controller = controllerTestRule.createController(session.getToken()); + PendingIntent sessionActivity = controller.getSessionActivity(); + assertThat(sessionActivity).isNotNull(); + if (Build.VERSION.SDK_INT >= 17) { + // PendingIntent#getCreatorPackage() is added in API 17. + assertThat(sessionActivity.getCreatorPackage()).isEqualTo(SUPPORT_APP_PACKAGE_NAME); + + // TODO: Add getPid/getUid in MediaControllerProviderService and compare them. + // assertThat(sessionActivity.getCreatorUid()).isEqualTo(remoteSession.getUid()); + } + session.cleanUp(); + } + + @Test + public void getPackageName() throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + assertThat(controller.getConnectedToken().getPackageName()).isEqualTo(SUPPORT_APP_PACKAGE_NAME); + } + + @Test + public void getTokenExtras() throws Exception { + Bundle testTokenExtras = TestUtils.createTestBundle(); + RemoteMediaSession session = createRemoteMediaSession("testGetExtras", testTokenExtras); + + MediaController controller = controllerTestRule.createController(session.getToken()); + SessionToken connectedToken = controller.getConnectedToken(); + assertThat(connectedToken).isNotNull(); + assertThat(TestUtils.equals(testTokenExtras, connectedToken.getExtras())).isTrue(); + } + + @Test + public void isConnected() throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + assertThat(controller.isConnected()).isTrue(); + + remoteSession.release(); + controllerTestRule.waitForDisconnect(controller, true); + assertThat(controller.isConnected()).isFalse(); + } + + @Test + public void close_beforeConnected() throws Exception { + MediaController controller = + controllerTestRule.createController( + remoteSession.getToken(), /* waitForConnect= */ false, null, /* callback= */ null); + threadTestRule.getHandler().postAndSync(controller::release); + } + + @Test + public void close_twice() throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + threadTestRule.getHandler().postAndSync(controller::release); + threadTestRule.getHandler().postAndSync(controller::release); + } + + @Test + public void gettersAfterConnected() throws Exception { + long currentPositionMs = 11; + long contentPositionMs = 33; + long durationMs = 200; + long bufferedPositionMs = 100; + int bufferedPercentage = 50; + long totalBufferedDurationMs = 120; + long currentLiveOffsetMs = 10; + long contentDurationMs = 300; + long contentBufferedPositionMs = 240; + boolean isPlayingAd = true; + int currentAdGroupIndex = 33; + int currentAdIndexInAdGroup = 22; + PlaybackParameters playbackParameters = new PlaybackParameters(/* speed= */ 0.5f); + boolean playWhenReady = true; + @Player.PlaybackSuppressionReason + int playbackSuppressionReason = Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS; + @Player.State int playbackState = Player.STATE_READY; + boolean isPlaying = true; + boolean isLoading = true; + MediaItem currentMediaItem = MediaTestUtils.createConvergedMediaItem(/* mediaId= */ "current"); + boolean isShuffleModeEnabled = true; + @RepeatMode int repeatMode = Player.REPEAT_MODE_ONE; + + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setCurrentPosition(currentPositionMs) + .setContentPosition(contentPositionMs) + .setDuration(durationMs) + .setBufferedPosition(bufferedPositionMs) + .setBufferedPercentage(bufferedPercentage) + .setTotalBufferedDuration(totalBufferedDurationMs) + .setCurrentLiveOffset(currentLiveOffsetMs) + .setContentDuration(contentDurationMs) + .setContentBufferedPosition(contentBufferedPositionMs) + .setIsPlayingAd(isPlayingAd) + .setCurrentAdGroupIndex(currentAdGroupIndex) + .setCurrentAdIndexInAdGroup(currentAdIndexInAdGroup) + .setPlaybackParameters(playbackParameters) + .setCurrentMediaItem(currentMediaItem) + .setPlayWhenReady(playWhenReady) + .setPlaybackSuppressionReason(playbackSuppressionReason) + .setPlaybackState(playbackState) + .setIsPlaying(isPlaying) + .setIsLoading(isLoading) + .setShuffleModeEnabled(isShuffleModeEnabled) + .setRepeatMode(repeatMode) + .build(); + remoteSession.setPlayer(playerConfig); + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + threadTestRule.getHandler().postAndSync(() -> controller.setTimeDiffMs(0L)); + + AtomicLong currentPositionMsRef = new AtomicLong(); + AtomicLong contentPositionMsRef = new AtomicLong(); + AtomicLong durationMsRef = new AtomicLong(); + AtomicLong bufferedPositionMsRef = new AtomicLong(); + AtomicInteger bufferedPercentageRef = new AtomicInteger(); + AtomicLong totalBufferedDurationMsRef = new AtomicLong(); + AtomicLong currentLiveOffsetMsRef = new AtomicLong(); + AtomicLong contentDurationMsRef = new AtomicLong(); + AtomicLong contentBufferedPositionMsRef = new AtomicLong(); + AtomicBoolean isPlayingAdRef = new AtomicBoolean(); + AtomicInteger currentAdGroupIndexRef = new AtomicInteger(); + AtomicInteger currentAdIndexInAdGroupRef = new AtomicInteger(); + AtomicReference playbackParametersRef = new AtomicReference<>(); + AtomicReference mediaItemRef = new AtomicReference<>(); + AtomicBoolean playWhenReadyRef = new AtomicBoolean(); + AtomicInteger playbackSuppressionReasonRef = new AtomicInteger(); + AtomicInteger playbackStateRef = new AtomicInteger(); + AtomicBoolean isPlayingRef = new AtomicBoolean(); + AtomicBoolean isLoadingRef = new AtomicBoolean(); + AtomicBoolean isShuffleModeEnabledRef = new AtomicBoolean(); + AtomicInteger repeatModeRef = new AtomicInteger(); + threadTestRule + .getHandler() + .postAndSync( + () -> { + currentPositionMsRef.set(controller.getCurrentPosition()); + contentPositionMsRef.set(controller.getContentPosition()); + durationMsRef.set(controller.getDuration()); + bufferedPositionMsRef.set(controller.getBufferedPosition()); + bufferedPercentageRef.set(controller.getBufferedPercentage()); + totalBufferedDurationMsRef.set(controller.getTotalBufferedDuration()); + currentLiveOffsetMsRef.set(controller.getCurrentLiveOffset()); + contentDurationMsRef.set(controller.getContentDuration()); + contentBufferedPositionMsRef.set(controller.getContentBufferedPosition()); + playbackParametersRef.set(controller.getPlaybackParameters()); + isPlayingAdRef.set(controller.isPlayingAd()); + currentAdGroupIndexRef.set(controller.getCurrentAdGroupIndex()); + currentAdIndexInAdGroupRef.set(controller.getCurrentAdIndexInAdGroup()); + mediaItemRef.set(controller.getCurrentMediaItem()); + playWhenReadyRef.set(controller.getPlayWhenReady()); + playbackSuppressionReasonRef.set(controller.getPlaybackSuppressionReason()); + playbackStateRef.set(controller.getPlaybackState()); + isPlayingRef.set(controller.isPlaying()); + isLoadingRef.set(controller.isLoading()); + isShuffleModeEnabledRef.set(controller.getShuffleModeEnabled()); + repeatModeRef.set(controller.getRepeatMode()); + }); + + assertThat(currentPositionMsRef.get()).isEqualTo(currentPositionMs); + assertThat(contentPositionMsRef.get()).isEqualTo(contentPositionMs); + assertThat(durationMsRef.get()).isEqualTo(durationMs); + assertThat(bufferedPositionMsRef.get()).isEqualTo(bufferedPositionMs); + assertThat(bufferedPercentageRef.get()).isEqualTo(bufferedPercentage); + assertThat(totalBufferedDurationMsRef.get()).isEqualTo(totalBufferedDurationMs); + assertThat(currentLiveOffsetMsRef.get()).isEqualTo(currentLiveOffsetMs); + assertThat(contentDurationMsRef.get()).isEqualTo(contentDurationMs); + assertThat(contentBufferedPositionMsRef.get()).isEqualTo(contentBufferedPositionMs); + assertThat(playbackParametersRef.get()).isEqualTo(playbackParameters); + assertThat(isPlayingAdRef.get()).isEqualTo(isPlayingAd); + assertThat(currentAdGroupIndexRef.get()).isEqualTo(currentAdGroupIndex); + assertThat(currentAdIndexInAdGroupRef.get()).isEqualTo(currentAdIndexInAdGroup); + MediaTestUtils.assertMediaIdEquals(currentMediaItem, mediaItemRef.get()); + assertThat(playWhenReadyRef.get()).isEqualTo(playWhenReady); + assertThat(playbackSuppressionReasonRef.get()).isEqualTo(playbackSuppressionReason); + assertThat(playbackStateRef.get()).isEqualTo(playbackState); + assertThat(isPlayingRef.get()).isEqualTo(isPlaying); + assertThat(isLoadingRef.get()).isEqualTo(isLoading); + assertThat(isShuffleModeEnabledRef.get()).isEqualTo(isShuffleModeEnabled); + assertThat(repeatModeRef.get()).isEqualTo(repeatMode); + } + + @Test + public void getPlayerError() throws Exception { + ExoPlaybackException testPlayerError = ExoPlaybackException.createForRemote("test"); + + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder().setPlayerError(testPlayerError).build(); + remoteSession.setPlayer(playerConfig); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + ExoPlaybackException playerError = + threadTestRule.getHandler().postAndSync(controller::getPlayerError); + assertThat(TestUtils.equals(playerError, testPlayerError)).isTrue(); + } + + @Test + public void getVideoSize_returnsVideoSizeOfPlayerInSession() throws Exception { + VideoSize testVideoSize = + new VideoSize( + /* width= */ 100, + /* height= */ 42, + /* unappliedRotationDegrees= */ 90, + /* pixelWidthHeightRatio= */ 1.2f); + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder().setVideoSize(testVideoSize).build(); + remoteSession.setPlayer(playerConfig); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + VideoSize videoSize = threadTestRule.getHandler().postAndSync(controller::getVideoSize); + assertThat(videoSize).isEqualTo(testVideoSize); + } + + @Test + public void futuresCompleted_AvailableCommandsChange() throws Exception { + RemoteMediaSession session = remoteSession; + MediaController controller = controllerTestRule.createController(session.getToken()); + + SessionCommands.Builder builder = new SessionCommands.Builder(); + SessionCommand setRatingCommand = + new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING); + SessionCommand customCommand = new SessionCommand("custom", null); + + int trials = 100; + CountDownLatch latch = new CountDownLatch(trials * 2); + + for (int trial = 0; trial < trials; trial++) { + if (trial % 2 == 0) { + builder.add(setRatingCommand); + builder.add(customCommand); + } else { + builder.remove(setRatingCommand); + builder.remove(customCommand); + } + session.setAvailableCommands(builder.build(), Player.Commands.EMPTY); + + String testMediaId = "testMediaId"; + Rating testRating = new HeartRating(/* hasHeart= */ true); + controller.setRating(testMediaId, testRating).addListener(latch::countDown, Runnable::run); + controller + .sendCustomCommand(customCommand, null) + .addListener(latch::countDown, Runnable::run); + } + + assertWithMessage("All futures should be completed") + .that(latch.await(LONG_TIMEOUT_MS, MILLISECONDS)) + .isTrue(); + } + + @Test + public void getPlaylistMetadata_returnsPlaylistMetadataOfPlayerInSession() throws Exception { + MediaMetadata playlistMetadata = new MediaMetadata.Builder().setTitle("title").build(); + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setPlaylistMetadata(playlistMetadata) + .build(); + remoteSession.setPlayer(playerConfig); + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + + assertThat(threadTestRule.getHandler().postAndSync(controller::getPlaylistMetadata)) + .isEqualTo(playlistMetadata); + } + + @Test + public void getAudioAttributes_returnsAudioAttributesOfPlayerInSession() throws Exception { + AudioAttributes testAttributes = + new AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MUSIC).build(); + + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder().setAudioAttributes(testAttributes).build(); + remoteSession.setPlayer(playerConfig); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + AudioAttributes attributes = + threadTestRule.getHandler().postAndSync(controller::getAudioAttributes); + assertThat(attributes).isEqualTo(testAttributes); + } + + @Test + public void getVolume_returnsVolumeOfPlayerInSession() throws Exception { + float testVolume = .5f; + + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder().setVolume(testVolume).build(); + remoteSession.setPlayer(playerConfig); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + float volume = threadTestRule.getHandler().postAndSync(controller::getVolume); + assertThat(volume).isEqualTo(testVolume); + } + + @Test + public void getCurrentWindowIndex() throws Exception { + int testWindowIndex = 1; + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setCurrentWindowIndex(testWindowIndex) + .build(); + remoteSession.setPlayer(playerConfig); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + int currentWindowIndex = + threadTestRule.getHandler().postAndSync(controller::getCurrentWindowIndex); + + assertThat(currentWindowIndex).isEqualTo(testWindowIndex); + } + + @Test + public void getCurrentPeriodIndex() throws Exception { + int testPeriodIndex = 1; + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setCurrentPeriodIndex(testPeriodIndex) + .build(); + remoteSession.setPlayer(playerConfig); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + int currentPeriodIndex = + threadTestRule.getHandler().postAndSync(controller::getCurrentPeriodIndex); + + assertThat(currentPeriodIndex).isEqualTo(testPeriodIndex); + } + + @Test + public void getPreviousWindowIndex() throws Exception { + Timeline timeline = MediaTestUtils.createTimeline(/* windowCount= */ 3); + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setTimeline(timeline) + .setCurrentWindowIndex(1) + .setRepeatMode(Player.REPEAT_MODE_OFF) + .setShuffleModeEnabled(false) + .build(); + remoteSession.setPlayer(playerConfig); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + int previousWindowIndex = + threadTestRule.getHandler().postAndSync(controller::getPreviousWindowIndex); + + assertThat(previousWindowIndex).isEqualTo(0); + } + + @Test + public void getPreviousWindowIndex_withRepeatModeOne() throws Exception { + Timeline timeline = MediaTestUtils.createTimeline(/* windowCount= */ 3); + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setTimeline(timeline) + .setCurrentWindowIndex(1) + .setRepeatMode(Player.REPEAT_MODE_ONE) + .setShuffleModeEnabled(false) + .build(); + remoteSession.setPlayer(playerConfig); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + int previousWindowIndex = + threadTestRule.getHandler().postAndSync(controller::getPreviousWindowIndex); + + assertThat(previousWindowIndex).isEqualTo(0); + } + + @Test + public void getPreviousWindowIndex_atTheFirstWindow() throws Exception { + Timeline timeline = MediaTestUtils.createTimeline(/* windowCount= */ 3); + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setTimeline(timeline) + .setCurrentWindowIndex(0) + .setRepeatMode(Player.REPEAT_MODE_OFF) + .setShuffleModeEnabled(false) + .build(); + remoteSession.setPlayer(playerConfig); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + int previousWindowIndex = + threadTestRule.getHandler().postAndSync(controller::getPreviousWindowIndex); + + assertThat(previousWindowIndex).isEqualTo(C.INDEX_UNSET); + } + + @Test + public void getPreviousWindowIndex_atTheFirstWindowWithRepeatModeAll() throws Exception { + Timeline timeline = MediaTestUtils.createTimeline(/* windowCount= */ 3); + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setTimeline(timeline) + .setCurrentWindowIndex(0) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .setShuffleModeEnabled(false) + .build(); + remoteSession.setPlayer(playerConfig); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + int previousWindowIndex = + threadTestRule.getHandler().postAndSync(controller::getPreviousWindowIndex); + + assertThat(previousWindowIndex).isEqualTo(2); + } + + @Test + public void getPreviousWindowIndex_withShuffleModeEnabled() throws Exception { + Timeline timeline = + new PlaylistTimeline( + MediaTestUtils.createConvergedMediaItems(/* size= */ 3), + /* shuffledIndices= */ new int[] {0, 2, 1}); + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setTimeline(timeline) + .setCurrentWindowIndex(2) + .setRepeatMode(Player.REPEAT_MODE_OFF) + .setShuffleModeEnabled(true) + .build(); + remoteSession.setPlayer(playerConfig); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + int previousWindowIndex = + threadTestRule.getHandler().postAndSync(controller::getPreviousWindowIndex); + + assertThat(previousWindowIndex).isEqualTo(0); + } + + @Test + public void getNextWindowIndex() throws Exception { + Timeline timeline = MediaTestUtils.createTimeline(/* windowCount= */ 3); + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setTimeline(timeline) + .setCurrentWindowIndex(1) + .setRepeatMode(Player.REPEAT_MODE_OFF) + .setShuffleModeEnabled(false) + .build(); + remoteSession.setPlayer(playerConfig); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + int nextWindowIndex = threadTestRule.getHandler().postAndSync(controller::getNextWindowIndex); + + assertThat(nextWindowIndex).isEqualTo(2); + } + + @Test + public void getNextWindowIndex_withRepeatModeOne() throws Exception { + Timeline timeline = MediaTestUtils.createTimeline(/* windowCount= */ 3); + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setTimeline(timeline) + .setCurrentWindowIndex(1) + .setRepeatMode(Player.REPEAT_MODE_ONE) + .setShuffleModeEnabled(false) + .build(); + remoteSession.setPlayer(playerConfig); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + int nextWindowIndex = threadTestRule.getHandler().postAndSync(controller::getNextWindowIndex); + + assertThat(nextWindowIndex).isEqualTo(2); + } + + @Test + public void getNextWindowIndex_atTheLastWindow() throws Exception { + Timeline timeline = MediaTestUtils.createTimeline(/* windowCount= */ 3); + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setTimeline(timeline) + .setCurrentWindowIndex(2) + .setRepeatMode(Player.REPEAT_MODE_OFF) + .setShuffleModeEnabled(false) + .build(); + remoteSession.setPlayer(playerConfig); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + int nextWindowIndex = threadTestRule.getHandler().postAndSync(controller::getNextWindowIndex); + + assertThat(nextWindowIndex).isEqualTo(C.INDEX_UNSET); + } + + @Test + public void getNextWindowIndex_atTheLastWindowWithRepeatModeAll() throws Exception { + Timeline timeline = MediaTestUtils.createTimeline(/* windowCount= */ 3); + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setTimeline(timeline) + .setCurrentWindowIndex(2) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .setShuffleModeEnabled(false) + .build(); + remoteSession.setPlayer(playerConfig); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + int nextWindowIndex = threadTestRule.getHandler().postAndSync(controller::getNextWindowIndex); + + assertThat(nextWindowIndex).isEqualTo(0); + } + + @Test + public void getNextWindowIndex_withShuffleModeEnabled() throws Exception { + Timeline timeline = + new PlaylistTimeline( + MediaTestUtils.createConvergedMediaItems(/* size= */ 3), + /* shuffledIndices= */ new int[] {0, 2, 1}); + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setTimeline(timeline) + .setCurrentWindowIndex(2) + .setRepeatMode(Player.REPEAT_MODE_OFF) + .setShuffleModeEnabled(true) + .build(); + remoteSession.setPlayer(playerConfig); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + int nextWindowIndex = threadTestRule.getHandler().postAndSync(controller::getNextWindowIndex); + + assertThat(nextWindowIndex).isEqualTo(1); + } + + @Test + public void getMediaItemCount() throws Exception { + int windowCount = 3; + Timeline timeline = MediaTestUtils.createTimeline(windowCount); + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder().setTimeline(timeline).build(); + remoteSession.setPlayer(playerConfig); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + int mediaItemCount = threadTestRule.getHandler().postAndSync(controller::getMediaItemCount); + + assertThat(mediaItemCount).isEqualTo(windowCount); + } + + @Test + public void getMediaItemAt() throws Exception { + int windowCount = 3; + int windowIndex = 1; + Timeline timeline = MediaTestUtils.createTimeline(windowCount); + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder().setTimeline(timeline).build(); + remoteSession.setPlayer(playerConfig); + + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + MediaItem mediaItem = + threadTestRule.getHandler().postAndSync(() -> controller.getMediaItemAt(windowIndex)); + + assertThat(mediaItem) + .isEqualTo(timeline.getWindow(windowIndex, new Timeline.Window()).mediaItem); + } + + private RemoteMediaSession createRemoteMediaSession(String id, Bundle tokenExtras) + throws Exception { + RemoteMediaSession session = new RemoteMediaSession(id, context, tokenExtras); + remoteSessionList.add(session); + return session; + } + + @Test + public void getCurrentPosition_whenNotPlaying_doesNotAdvance() throws Exception { + long testCurrentPositionMs = 100L; + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setIsPlaying(false) + .setCurrentPosition(testCurrentPositionMs) + .setDuration(10_000L) + .build(); + remoteSession.setPlayer(playerConfig); + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + + long currentPositionMs = + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setTimeDiffMs(50L); + return controller.getCurrentPosition(); + }); + + assertThat(currentPositionMs).isEqualTo(testCurrentPositionMs); + } + + @Test + public void getCurrentPosition_whenPlaying_advances() throws Exception { + long testCurrentPosition = 100L; + PlaybackParameters testPlaybackParameters = new PlaybackParameters(/* speed= */ 2.0f); + long testTimeDiff = 50L; + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setIsPlaying(true) + .setCurrentPosition(testCurrentPosition) + .setDuration(10_000L) + .setPlaybackParameters(testPlaybackParameters) + .build(); + remoteSession.setPlayer(playerConfig); + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + + long currentPositionMs = + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setTimeDiffMs(testTimeDiff); + return controller.getCurrentPosition(); + }); + + long expectedCurrentPositionMs = + testCurrentPosition + (long) (testTimeDiff * testPlaybackParameters.speed); + assertThat(currentPositionMs).isEqualTo(expectedCurrentPositionMs); + } + + @Test + public void getContentPosition_whenPlayingAd_doesNotAdvance() throws Exception { + long testContentPosition = 100L; + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setContentPosition(testContentPosition) + .setDuration(10_000L) + .setIsPlaying(true) + .setIsPlayingAd(true) + .setPlaybackParameters(new PlaybackParameters(/* speed= */ 2.0f)) + .build(); + remoteSession.setPlayer(playerConfig); + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + + long contentPositionMs = + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setTimeDiffMs(50L); + return controller.getContentPosition(); + }); + + assertThat(contentPositionMs).isEqualTo(testContentPosition); + } + + @Test + public void getBufferedPosition_withPeriodicUpdate_updatedWithoutCallback() throws Exception { + long testBufferedPosition = 999L; + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + remoteSession.getMockPlayer().setBufferedPosition(testBufferedPosition); + + remoteSession.setSessionPositionUpdateDelayMs(0L); + + PollingCheck.waitFor( + TIMEOUT_MS, + () -> { + long bufferedPosition = + threadTestRule.getHandler().postAndSync(controller::getBufferedPosition); + return bufferedPosition == testBufferedPosition; + }); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaControllerTestRule.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaControllerTestRule.java new file mode 100644 index 0000000000..ec8a594d1f --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaControllerTestRule.java @@ -0,0 +1,202 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.session; + +import android.content.Context; +import android.os.Bundle; +import android.support.v4.media.session.MediaSessionCompat; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.collection.ArrayMap; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.exoplayer2.session.MediaController.ControllerCallback; +import com.google.android.exoplayer2.session.vct.common.HandlerThreadTestRule; +import com.google.android.exoplayer2.util.Log; +import java.util.Map; +import org.junit.rules.ExternalResource; + +/** + * TestRule for managing {@link MediaController} instances. This class is not thread-safe, so call + * its methods on the junit test thread only. + */ +public final class MediaControllerTestRule extends ExternalResource { + private static final String TAG = "MediaControllerTestRule"; + + private final HandlerThreadTestRule handlerThreadTestRule; + private final Map controllers = new ArrayMap<>(); + private volatile Context context; + private volatile Class controllerType = MediaController.class; + + public MediaControllerTestRule(HandlerThreadTestRule handlerThreadTestRule) { + this.handlerThreadTestRule = handlerThreadTestRule; + } + + @Override + protected void before() { + context = ApplicationProvider.getApplicationContext(); + } + + @Override + protected void after() { + for (MediaController controller : controllers.keySet()) { + try { + handlerThreadTestRule.getHandler().postAndSync(controller::release); + } catch (Exception e) { + Log.e(TAG, "Exception in release", e); + } + } + controllers.clear(); + } + + /** + * Sets a subtype of {@link MediaController} to be instantiated by {@link #createController}. It + * can be either {@link MediaController} or {@link MediaBrowser}. The default is {@link + * MediaController}. + */ + public void setControllerType(Class controllerType) { + if (!(controllerType == MediaController.class || controllerType == MediaBrowser.class)) { + throw new IllegalArgumentException("Illegal controllerType, " + controllerType); + } + this.controllerType = controllerType; + } + + /** + * Creates {@link MediaController} from {@link MediaSessionCompat.Token} with default options + * waiting for connection. + */ + @NonNull + public MediaController createController(@NonNull MediaSessionCompat.Token token) + throws Exception { + return createController(token, /* waitForConnect= */ true, /* callback= */ null); + } + + /** Creates {@link MediaController} from {@link MediaSessionCompat.Token}. */ + @NonNull + public MediaController createController( + @NonNull MediaSessionCompat.Token token, + boolean waitForConnect, + @Nullable ControllerCallback callback) + throws Exception { + TestBrowserCallback testCallback = new TestBrowserCallback(callback); + MediaController controller = createControllerOnHandler(token, testCallback); + controllers.put(controller, testCallback); + if (waitForConnect) { + testCallback.waitForConnect(true); + } + return controller; + } + + @NonNull + private MediaController createControllerOnHandler( + @NonNull MediaSessionCompat.Token token, @NonNull TestBrowserCallback callback) + throws Exception { + // Create controller on the test handler, for changing MediaBrowserCompat's Handler + // Looper. Otherwise, MediaBrowserCompat will post all the commands to the handler + // and commands wouldn't be run if tests codes waits on the test handler. + return handlerThreadTestRule + .getHandler() + .postAndSync( + () -> { + if (controllerType == MediaBrowser.class) { + return new MediaBrowser.Builder(context) + .setSessionCompatToken(token) + .setControllerCallback(callback) + .build(); + } else { + return new MediaController.Builder(context) + .setSessionCompatToken(token) + .setControllerCallback(callback) + .build(); + } + }); + } + + /** + * Creates {@link MediaController} from {@link SessionToken} with default options waiting for + * connection. + */ + @NonNull + public MediaController createController(@NonNull SessionToken token) throws Exception { + return createController( + token, /* waitForConnect= */ true, /* connectionHints= */ null, /* callback= */ null); + } + + /** Creates {@link MediaController} from {@link SessionToken}. */ + @NonNull + public MediaController createController( + @NonNull SessionToken token, + boolean waitForConnect, + @Nullable Bundle connectionHints, + @Nullable ControllerCallback callback) + throws Exception { + TestBrowserCallback testCallback = new TestBrowserCallback(callback); + MediaController controller = createControllerOnHandler(token, connectionHints, testCallback); + controllers.put(controller, testCallback); + if (waitForConnect) { + testCallback.waitForConnect(true); + } + return controller; + } + + @NonNull + private MediaController createControllerOnHandler( + @NonNull SessionToken token, + @Nullable Bundle connectionHints, + @NonNull TestBrowserCallback callback) + throws Exception { + // Create controller on the test handler, for changing MediaBrowserCompat's Handler + // Looper. Otherwise, MediaBrowserCompat will post all the commands to the handler + // and commands wouldn't be run if tests codes waits on the test handler. + return handlerThreadTestRule + .getHandler() + .postAndSync( + () -> { + if (controllerType == MediaBrowser.class) { + MediaBrowser.Builder builder = + new MediaBrowser.Builder(context) + .setSessionToken(token) + .setControllerCallback(callback); + if (connectionHints != null) { + builder.setConnectionHints(connectionHints); + } + return builder.build(); + } else { + MediaController.Builder builder = + new MediaController.Builder(context) + .setSessionToken(token) + .setControllerCallback(callback); + if (connectionHints != null) { + builder.setConnectionHints(connectionHints); + } + return builder.build(); + } + }); + } + + public void waitForConnect(MediaController controller, boolean expected) + throws InterruptedException { + controllers.get(controller).waitForConnect(expected); + } + + public void waitForDisconnect(MediaController controller, boolean expected) + throws InterruptedException { + controllers.get(controller).waitForDisconnect(expected); + } + + public void setRunnableForOnCustomCommand(MediaController controller, Runnable runnable) { + controllers.get(controller).setRunnableForOnCustomCommand(runnable); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaControllerWithFrameworkMediaSessionTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaControllerWithFrameworkMediaSessionTest.java new file mode 100644 index 0000000000..55c8329b49 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaControllerWithFrameworkMediaSessionTest.java @@ -0,0 +1,157 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.Player.STATE_READY; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.SERVICE_CONNECTION_TIMEOUT_MS; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.TIMEOUT_MS; +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.Context; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; +import android.os.Build; +import android.os.HandlerThread; +import android.support.v4.media.session.MediaSessionCompat; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import androidx.test.filters.SdkSuppress; +import com.google.android.exoplayer2.Player.State; +import com.google.android.exoplayer2.session.vct.common.MainLooperTestRule; +import com.google.android.exoplayer2.session.vct.common.TestHandler; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link MediaController} with framework MediaSession, which exists since Android-L. */ +@RunWith(AndroidJUnit4.class) +@LargeTest +@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) // For framework MediaSession +public class MediaControllerWithFrameworkMediaSessionTest { + private static final String TAG = "MediaControllerWithFrameworkMediaSessionTest"; + + @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); + + private Context context; + private TestHandler handler; + private MediaSession fwkSession; + + @Before + public void setUp() { + context = ApplicationProvider.getApplicationContext(); + + HandlerThread handlerThread = new HandlerThread(TAG); + handlerThread.start(); + TestHandler handler = new TestHandler(handlerThread.getLooper()); + this.handler = handler; + + fwkSession = new android.media.session.MediaSession(context, TAG); + fwkSession.setActive(true); + fwkSession.setFlags( + MediaSession.FLAG_HANDLES_MEDIA_BUTTONS | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); + fwkSession.setCallback(new android.media.session.MediaSession.Callback() {}, handler); + } + + @After + public void cleanUp() { + if (fwkSession != null) { + fwkSession.release(); + fwkSession = null; + } + if (handler != null) { + if (Build.VERSION.SDK_INT >= 18) { + handler.getLooper().quitSafely(); + } else { + handler.getLooper().quit(); + } + handler = null; + } + } + + @Test + public void onConnected_calledAfterCreated() throws Exception { + CountDownLatch connectedLatch = new CountDownLatch(1); + MediaController.ControllerCallback callback = + new MediaController.ControllerCallback() { + @Override + public void onConnected(MediaController controller) { + connectedLatch.countDown(); + } + }; + MediaController controller = + new MediaController.Builder(context) + .setSessionCompatToken(MediaSessionCompat.Token.fromToken(fwkSession.getSessionToken())) + .setControllerCallback(callback) + .setApplicationLooper(handler.getLooper()) + .build(); + try { + assertThat(connectedLatch.await(SERVICE_CONNECTION_TIMEOUT_MS, MILLISECONDS)).isTrue(); + } finally { + handler.postAndSync(controller::release); + } + } + + @Test + public void onPlaybackStateChanged_isNotifiedByFwkSessionChanges() throws Exception { + CountDownLatch connectedLatch = new CountDownLatch(1); + CountDownLatch playbackStateChangedLatch = new CountDownLatch(1); + AtomicInteger playbackStateRef = new AtomicInteger(); + AtomicBoolean playWhenReadyRef = new AtomicBoolean(); + MediaController.ControllerCallback callback = + new MediaController.ControllerCallback() { + @Override + public void onConnected(MediaController controller) { + connectedLatch.countDown(); + } + }; + MediaController controller = + new MediaController.Builder(context) + .setSessionCompatToken(MediaSessionCompat.Token.fromToken(fwkSession.getSessionToken())) + .setControllerCallback(callback) + .setApplicationLooper(handler.getLooper()) + .build(); + SessionPlayer.PlayerCallback playerCallback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPlaybackStateChanged(@State int state) { + playbackStateRef.set(state); + playWhenReadyRef.set(controller.getPlayWhenReady()); + playbackStateChangedLatch.countDown(); + } + }; + try { + controller.addListener(playerCallback); + assertThat(connectedLatch.await(SERVICE_CONNECTION_TIMEOUT_MS, MILLISECONDS)).isTrue(); + fwkSession.setPlaybackState( + new PlaybackState.Builder() + .setState(PlaybackState.STATE_PLAYING, /* position= */ 0, /* playbackSpeed= */ 1.0f) + .build()); + assertThat(playbackStateChangedLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(playbackStateRef.get()).isEqualTo(STATE_READY); + assertThat(playWhenReadyRef.get()).isTrue(); + } finally { + handler.postAndSync(controller::release); + } + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaControllerWithMediaSessionCompatTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaControllerWithMediaSessionCompatTest.java new file mode 100644 index 0000000000..292cc74553 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaControllerWithMediaSessionCompatTest.java @@ -0,0 +1,1336 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.Player.STATE_BUFFERING; +import static com.google.android.exoplayer2.Player.STATE_READY; +import static com.google.android.exoplayer2.session.LegacyMediaMetadata.METADATA_KEY_ADVERTISEMENT; +import static com.google.android.exoplayer2.session.LegacyMediaMetadata.METADATA_KEY_DURATION; +import static com.google.android.exoplayer2.session.LegacyMediaMetadata.METADATA_KEY_MEDIA_ID; +import static com.google.android.exoplayer2.session.MediaConstants.ARGUMENT_CAPTIONING_ENABLED; +import static com.google.android.exoplayer2.session.MediaConstants.SESSION_COMMAND_ON_CAPTIONING_ENABLED_CHANGED; +import static com.google.android.exoplayer2.session.SessionResult.RESULT_INFO_SKIPPED; +import static com.google.android.exoplayer2.session.SessionResult.RESULT_SUCCESS; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.DEFAULT_TEST_NAME; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.NO_RESPONSE_TIMEOUT_MS; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.TIMEOUT_MS; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.MediaSessionCompat.QueueItem; +import android.support.v4.media.session.PlaybackStateCompat; +import android.support.v4.media.session.PlaybackStateCompat.CustomAction; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.media.VolumeProviderCompat; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.ext.truth.os.BundleSubject; +import androidx.test.filters.MediumTest; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaMetadata; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.PositionInfo; +import com.google.android.exoplayer2.Player.RepeatMode; +import com.google.android.exoplayer2.Player.State; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.device.DeviceInfo; +import com.google.android.exoplayer2.session.MediaController.ControllerCallback; +import com.google.android.exoplayer2.session.vct.common.HandlerThreadTestRule; +import com.google.android.exoplayer2.session.vct.common.MainLooperTestRule; +import com.google.android.exoplayer2.session.vct.common.MockActivity; +import com.google.android.exoplayer2.session.vct.common.TestUtils; +import com.google.common.collect.Range; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; + +/** Tests for {@link MediaController} interacting with {@link MediaSessionCompat}. */ +@RunWith(AndroidJUnit4.class) +@MediumTest +public class MediaControllerWithMediaSessionCompatTest { + + private static final String TAG = "MCwMSCTest"; + + @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); + + private final HandlerThreadTestRule threadTestRule = new HandlerThreadTestRule(TAG); + private final MediaControllerTestRule controllerTestRule = + new MediaControllerTestRule(threadTestRule); + + @Rule + public final TestRule chain = RuleChain.outerRule(threadTestRule).around(controllerTestRule); + + private Context context; + private RemoteMediaSessionCompat session; + + @Before + public void setUp() throws Exception { + context = ApplicationProvider.getApplicationContext(); + session = new RemoteMediaSessionCompat(DEFAULT_TEST_NAME, context); + } + + @After + public void cleanUp() throws Exception { + session.cleanUp(); + } + + @Test + public void connected() throws Exception { + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + assertThat(controller.isConnected()).isTrue(); + } + + @Test + public void disconnected_bySessionRelease() throws Exception { + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + threadTestRule.getHandler().postAndSync(controller::release); + controllerTestRule.waitForDisconnect(controller, /* expected= */ true); + assertThat(controller.isConnected()).isFalse(); + } + + @Test + public void disconnected_byControllerClose() throws Exception { + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + threadTestRule.getHandler().postAndSync(controller::release); + controllerTestRule.waitForDisconnect(controller, /* expected= */ true); + assertThat(controller.isConnected()).isFalse(); + } + + @Test + public void close_twice_doesNotCrash() throws Exception { + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + threadTestRule.getHandler().postAndSync(controller::release); + threadTestRule.getHandler().postAndSync(controller::release); + } + + @Test + public void close_beforeConnected_doesNotCrash() throws Exception { + MediaController controller = + controllerTestRule.createController( + session.getSessionToken(), /* waitForConnect= */ false, /* callback= */ null); + threadTestRule.getHandler().postAndSync(controller::release); + } + + @Test + public void gettersAfterConnected() throws Exception { + long position = 150_000; + long bufferedPosition = 900_000; + long duration = 1_000_000; + float speed = 1.5f; + CharSequence queueTitle = "queueTitle"; + @PlaybackStateCompat.ShuffleMode int shuffleMode = PlaybackStateCompat.SHUFFLE_MODE_GROUP; + @PlaybackStateCompat.RepeatMode int repeatMode = PlaybackStateCompat.REPEAT_MODE_GROUP; + boolean isPlayingAd = true; + + MediaMetadataCompat metadata = + MediaUtils.convertToMediaMetadataCompat( + new LegacyMediaMetadata.Builder() + .putString(METADATA_KEY_MEDIA_ID, "gettersAfterConnected") + .putLong(METADATA_KEY_DURATION, duration) + .putLong(METADATA_KEY_ADVERTISEMENT, isPlayingAd ? 1 : 0) + .build()); + + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setState(PlaybackStateCompat.STATE_PLAYING, position, speed) + .setBufferedPosition(bufferedPosition) + .build()); + session.setMetadata(metadata); + session.setQueueTitle(queueTitle); + session.setShuffleMode(shuffleMode); + session.setRepeatMode(repeatMode); + + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + + AtomicInteger playerStateRef = new AtomicInteger(); + AtomicInteger bufferingStateRef = new AtomicInteger(); + AtomicLong positionRef = new AtomicLong(); + AtomicLong bufferedPositionRef = new AtomicLong(); + AtomicReference speedRef = new AtomicReference<>(); + AtomicReference mediaItemRef = new AtomicReference<>(); + AtomicBoolean playWhenReadyRef = new AtomicBoolean(); + AtomicLong playbackStateRef = new AtomicLong(); + AtomicBoolean shuffleModeEnabledRef = new AtomicBoolean(); + AtomicLong repeatModeRef = new AtomicLong(); + AtomicReference playlistMetadataRef = new AtomicReference<>(); + AtomicBoolean isPlayingAdRef = new AtomicBoolean(); + threadTestRule + .getHandler() + .postAndSync( + () -> { + positionRef.set(controller.getCurrentPosition()); + bufferedPositionRef.set(controller.getBufferedPosition()); + speedRef.set(controller.getPlaybackParameters().speed); + mediaItemRef.set(controller.getCurrentMediaItem()); + playWhenReadyRef.set(controller.getPlayWhenReady()); + playbackStateRef.set(controller.getPlaybackState()); + repeatModeRef.set(controller.getRepeatMode()); + shuffleModeEnabledRef.set(controller.getShuffleModeEnabled()); + playlistMetadataRef.set(controller.getPlaylistMetadata()); + isPlayingAdRef.set(controller.isPlayingAd()); + }); + + assertThat(positionRef.get()) + .isIn(Range.closedOpen(position, position + (long) (speed * TIMEOUT_MS))); + assertThat(bufferedPositionRef.get()).isEqualTo(bufferedPosition); + assertThat(speedRef.get()).isEqualTo(speed); + assertThat(mediaItemRef.get().mediaId).isEqualTo(metadata.getDescription().getMediaId()); + assertThat(playWhenReadyRef.get()).isTrue(); + assertThat(playbackStateRef.get()).isEqualTo(STATE_READY); + assertThat(shuffleModeEnabledRef.get()).isTrue(); + assertThat(repeatModeRef.get()).isEqualTo(Player.REPEAT_MODE_ALL); + assertThat(playlistMetadataRef.get().title.toString()).isEqualTo(queueTitle.toString()); + assertThat(isPlayingAdRef.get()).isEqualTo(isPlayingAd); + } + + @Test + public void getPackageName() throws Exception { + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + assertThat(controller.getConnectedToken().getPackageName()).isEqualTo(SUPPORT_APP_PACKAGE_NAME); + } + + @Test + public void getSessionActivity() throws Exception { + Intent sessionActivity = new Intent(context, MockActivity.class); + PendingIntent pi = PendingIntent.getActivity(context, 0, sessionActivity, 0); + session.setSessionActivity(pi); + + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + PendingIntent sessionActivityOut = controller.getSessionActivity(); + assertThat(sessionActivityOut).isNotNull(); + if (Build.VERSION.SDK_INT >= 17) { + // PendingIntent#getCreatorPackage() is added in API 17. + assertThat(sessionActivityOut.getCreatorPackage()).isEqualTo(context.getPackageName()); + } + } + + @Test + public void setRepeatMode_updatesAndNotifiesRepeatMode() throws Exception { + @Player.RepeatMode int testRepeatMode = Player.REPEAT_MODE_ALL; + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger repeatModeFromParamRef = new AtomicInteger(); + AtomicInteger repeatModeFromGetterRef = new AtomicInteger(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onRepeatModeChanged(@RepeatMode int repeatMode) { + repeatModeFromParamRef.set(repeatMode); + repeatModeFromGetterRef.set(controller.getRepeatMode()); + latch.countDown(); + } + }; + controller.addListener(callback); + + session.setRepeatMode(PlaybackStateCompat.REPEAT_MODE_GROUP); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(repeatModeFromParamRef.get()).isEqualTo(testRepeatMode); + assertThat(repeatModeFromGetterRef.get()).isEqualTo(testRepeatMode); + } + + @Test + public void setShuffleModeEnabled_updatesAndNotifiesShuffleModeEnabled() throws Exception { + boolean testShuffleModeEnabled = true; + + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean shuffleModeFromParamRef = new AtomicBoolean(); + AtomicBoolean shuffleModeFromGetterRef = new AtomicBoolean(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + shuffleModeFromParamRef.set(shuffleModeEnabled); + shuffleModeFromGetterRef.set(controller.getShuffleModeEnabled()); + latch.countDown(); + } + }; + controller.addListener(callback); + + session.setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_ALL); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(shuffleModeFromParamRef.get()).isEqualTo(testShuffleModeEnabled); + assertThat(shuffleModeFromGetterRef.get()).isEqualTo(testShuffleModeEnabled); + } + + @Test + public void setQueue_updatesAndNotifiesTimeline() throws Exception { + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference timelineFromParamRef = new AtomicReference<>(); + AtomicReference timelineFromGetterRef = new AtomicReference<>(); + AtomicInteger reasonRef = new AtomicInteger(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onTimelineChanged( + Timeline timeline, @Player.TimelineChangeReason int reason) { + timelineFromParamRef.set(timeline); + timelineFromGetterRef.set(controller.getCurrentTimeline()); + reasonRef.set(reason); + latch.countDown(); + } + }; + controller.addListener(callback); + + Timeline testTimeline = MediaTestUtils.createTimeline(/* windowCount= */ 2); + List testQueue = + MediaUtils.convertToQueueItemList(MediaUtils.convertToMediaItemList(testTimeline)); + session.setQueue(testQueue); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + MediaTestUtils.assertMediaIdEquals(testTimeline, timelineFromParamRef.get()); + MediaTestUtils.assertMediaIdEquals(testTimeline, timelineFromParamRef.get()); + assertThat(reasonRef.get()).isEqualTo(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + } + + @Test + public void setQueue_withNull_notifiesEmptyTimeline() throws Exception { + Timeline timeline = MediaTestUtils.createTimeline(/* windowCount= */ 2); + List queue = + MediaUtils.convertToQueueItemList(MediaUtils.convertToMediaItemList(timeline)); + session.setQueue(queue); + + CountDownLatch latch = new CountDownLatch(1); + AtomicReference timelineRef = new AtomicReference<>(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onTimelineChanged( + Timeline timeline, @Player.TimelineChangeReason int reason) { + timelineRef.set(timeline); + latch.countDown(); + } + }; + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + controller.addListener(callback); + + session.setQueue(null); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(timelineRef.get().getWindowCount()).isEqualTo(0); + assertThat(timelineRef.get().getPeriodCount()).isEqualTo(0); + } + + @Test + public void setQueueTitle_updatesAndNotifiesPlaylistMetadata() throws Exception { + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference metadataFromParamRef = new AtomicReference<>(); + AtomicReference metadataFromGetterRef = new AtomicReference<>(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPlaylistMetadataChanged(MediaMetadata playlistMetadata) { + metadataFromParamRef.set(playlistMetadata); + metadataFromGetterRef.set(controller.getPlaylistMetadata()); + latch.countDown(); + } + }; + controller.addListener(callback); + + CharSequence queueTitle = "queueTitle"; + session.setQueueTitle(queueTitle); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(metadataFromParamRef.get().title.toString()).isEqualTo(queueTitle.toString()); + assertThat(metadataFromGetterRef.get().title.toString()).isEqualTo(queueTitle.toString()); + } + + @Test + public void getCurrentMediaItem_byDefault_returnsNull() throws Exception { + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + MediaItem mediaItem = threadTestRule.getHandler().postAndSync(controller::getCurrentMediaItem); + assertThat(mediaItem).isNull(); + } + + @Test + public void getCurrentMediaItem_withSetMetadata_returnsMediaItemWithMediaId() throws Exception { + String testMediaId = "testMediaId"; + MediaMetadataCompat metadata = + new MediaMetadataCompat.Builder().putText(METADATA_KEY_MEDIA_ID, testMediaId).build(); + session.setMetadata(metadata); + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + + MediaItem mediaItem = threadTestRule.getHandler().postAndSync(controller::getCurrentMediaItem); + + assertThat(mediaItem.mediaId).isEqualTo(testMediaId); + } + + @Test + public void setMetadata_notifiesCurrentMediaItem() throws Exception { + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference itemRef = new AtomicReference<>(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onMediaItemTransition( + @Nullable MediaItem item, @Player.MediaItemTransitionReason int reason) { + itemRef.set(item); + latch.countDown(); + } + }; + controller.addListener(callback); + + String testMediaId = "testMediaId"; + MediaMetadataCompat metadata = + new MediaMetadataCompat.Builder().putText(METADATA_KEY_MEDIA_ID, testMediaId).build(); + session.setMetadata(metadata); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(itemRef.get().mediaId).isEqualTo(testMediaId); + } + + @Test + public void isPlayingAd_withMetadataWithAd_returnsTrue() throws Exception { + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean isPlayingAdRef = new AtomicBoolean(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPositionDiscontinuity( + PositionInfo oldPosition, PositionInfo newPosition, @DiscontinuityReason int reason) { + isPlayingAdRef.set(controller.isPlayingAd()); + latch.countDown(); + } + }; + controller.addListener(callback); + + MediaMetadataCompat metadata = + new MediaMetadataCompat.Builder() + .putLong(MediaMetadataCompat.METADATA_KEY_ADVERTISEMENT, 1) + .build(); + session.setMetadata(metadata); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(isPlayingAdRef.get()).isTrue(); + } + + @Test + public void setMediaUri_resultSetAfterPrepare() throws Exception { + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + + Uri testUri = Uri.parse("androidx://test"); + ListenableFuture future = + threadTestRule + .getHandler() + .postAndSync(() -> controller.setMediaUri(testUri, /* extras= */ null)); + + SessionResult result; + try { + result = future.get(NO_RESPONSE_TIMEOUT_MS, TimeUnit.MILLISECONDS); + assertWithMessage("TimeoutException is expected").fail(); + } catch (TimeoutException e) { + // expected. + } + + threadTestRule.getHandler().postAndSync(controller::prepare); + + result = future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); + } + + @Test + public void setMediaUri_resultSetAfterPlay() throws Exception { + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + + Uri testUri = Uri.parse("androidx://test"); + ListenableFuture future = + threadTestRule + .getHandler() + .postAndSync(() -> controller.setMediaUri(testUri, /* extras= */ null)); + + SessionResult result; + try { + result = future.get(NO_RESPONSE_TIMEOUT_MS, TimeUnit.MILLISECONDS); + assertWithMessage("TimeoutException is expected").fail(); + } catch (TimeoutException e) { + // expected. + } + + threadTestRule.getHandler().postAndSync(controller::play); + + result = future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); + } + + @Test + public void setMediaUris_multipleCalls_previousCallReturnsResultInfoSkipped() throws Exception { + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + + Uri testUri1 = Uri.parse("androidx://test1"); + Uri testUri2 = Uri.parse("androidx://test2"); + ListenableFuture future1 = + threadTestRule + .getHandler() + .postAndSync(() -> controller.setMediaUri(testUri1, /* extras= */ null)); + ListenableFuture future2 = + threadTestRule + .getHandler() + .postAndSync(() -> controller.setMediaUri(testUri2, /* extras= */ null)); + + threadTestRule.getHandler().postAndSync(controller::prepare); + + SessionResult result1 = future1.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + SessionResult result2 = future2.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + assertThat(result1.resultCode).isEqualTo(RESULT_INFO_SKIPPED); + assertThat(result2.resultCode).isEqualTo(RESULT_SUCCESS); + } + + @Test + public void seekToDefaultPosition_withWindowIndex_updatesExpectedWindowIndex() throws Exception { + List testList = MediaTestUtils.createConvergedMediaItems(3); + List testQueue = MediaUtils.convertToQueueItemList(testList); + session.setQueue(testQueue); + session.setPlaybackState(/* state= */ null); + int testWindowIndex = 2; + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger currentWindowIndexRef = new AtomicInteger(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPositionDiscontinuity( + PositionInfo oldPosition, PositionInfo newPosition, @DiscontinuityReason int reason) { + currentWindowIndexRef.set(controller.getCurrentWindowIndex()); + latch.countDown(); + } + }; + controller.addListener(callback); + threadTestRule + .getHandler() + .postAndSync(() -> controller.seekToDefaultPosition(testWindowIndex)); + + session.setMetadata( + new MediaMetadataCompat.Builder() + .putString( + MediaMetadataCompat.METADATA_KEY_MEDIA_ID, testList.get(testWindowIndex).mediaId) + .build()); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(currentWindowIndexRef.get()).isEqualTo(testWindowIndex); + } + + @Test + public void seekTo_withWindowIndex_updatesExpectedWindowIndex() throws Exception { + List testList = MediaTestUtils.createConvergedMediaItems(3); + List testQueue = MediaUtils.convertToQueueItemList(testList); + session.setQueue(testQueue); + session.setPlaybackState(/* state= */ null); + long testPositionMs = 23L; + int testWindowIndex = 2; + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger windowIndexFromParamRef = new AtomicInteger(); + AtomicInteger windowIndexFromGetterRef = new AtomicInteger(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPositionDiscontinuity( + PositionInfo oldPosition, PositionInfo newPosition, @DiscontinuityReason int reason) { + windowIndexFromParamRef.set(newPosition.windowIndex); + windowIndexFromGetterRef.set(controller.getCurrentWindowIndex()); + latch.countDown(); + } + }; + controller.addListener(callback); + threadTestRule + .getHandler() + .postAndSync(() -> controller.seekTo(testWindowIndex, testPositionMs)); + + session.setMetadata( + new MediaMetadataCompat.Builder() + .putString( + MediaMetadataCompat.METADATA_KEY_MEDIA_ID, testList.get(testWindowIndex).mediaId) + .build()); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + + assertThat(windowIndexFromParamRef.get()).isEqualTo(testWindowIndex); + assertThat(windowIndexFromGetterRef.get()).isEqualTo(testWindowIndex); + } + + @Test + public void setMetadata_withAd_notifiesPositionDiscontinuityByAdInsertion() throws Exception { + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger positionDiscontinuityReasonRef = new AtomicInteger(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPositionDiscontinuity( + PositionInfo oldPosition, PositionInfo newPosition, @DiscontinuityReason int reason) { + latch.countDown(); + } + }; + controller.addListener(callback); + + String testMediaId = "testMediaId"; + MediaMetadataCompat metadata = + new MediaMetadataCompat.Builder() + .putText(METADATA_KEY_MEDIA_ID, testMediaId) + .putLong(METADATA_KEY_ADVERTISEMENT, 1) + .build(); + session.setMetadata(metadata); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(positionDiscontinuityReasonRef.get()) + .isEqualTo(Player.DISCONTINUITY_REASON_AUTO_TRANSITION); + } + + @Test + public void setPlaybackState_withActiveQueueItemId_notifiesCurrentMediaItem() throws Exception { + List testList = MediaTestUtils.createConvergedMediaItems(/* size= */ 2); + List testQueue = MediaUtils.convertToQueueItemList(testList); + session.setQueue(testQueue); + + PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder(); + + // Set the current active queue item to index 'oldItemIndex'. + int oldItemIndex = 0; + builder.setActiveQueueItemId(testQueue.get(oldItemIndex).getQueueId()); + session.setPlaybackState(builder.build()); + + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference itemRef = new AtomicReference<>(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onMediaItemTransition( + @Nullable MediaItem item, @Player.MediaItemTransitionReason int reason) { + itemRef.set(item); + latch.countDown(); + } + }; + controller.addListener(callback); + + // The new playbackState will tell the controller that the active queue item is changed to + // 'newItemIndex'. + int newItemIndex = 1; + builder.setActiveQueueItemId(testQueue.get(newItemIndex).getQueueId()); + session.setPlaybackState(builder.build()); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + MediaTestUtils.assertMediaIdEquals(testList.get(newItemIndex), itemRef.get()); + } + + @Test + public void + setPlaybackState_withAdjacentQueueItemWhilePlaying_notifiesPositionDiscontinuityBySeek() + throws Exception { + long testDuration = 3000; + List testQueue = MediaTestUtils.createQueueItems(/* size= */ 2); + session.setQueue(testQueue); + session.setMetadata( + new MediaMetadataCompat.Builder() + .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, testDuration) + .build()); + + PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder(); + + // Set the current active queue item to index 'oldItemIndex'. + int oldItemIndex = 0; + builder.setActiveQueueItemId(testQueue.get(oldItemIndex).getQueueId()); + builder.setState( + PlaybackStateCompat.STATE_PLAYING, /* position= */ 0L, /* playbackSpeed= */ 1.0f); + session.setPlaybackState(builder.build()); + + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference oldPositionRef = new AtomicReference<>(); + AtomicReference newPositionRef = new AtomicReference<>(); + AtomicInteger positionDiscontinuityReasonRef = new AtomicInteger(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPositionDiscontinuity( + PositionInfo oldPosition, PositionInfo newPosition, @DiscontinuityReason int reason) { + oldPositionRef.set(oldPosition); + newPositionRef.set(newPosition); + positionDiscontinuityReasonRef.set(reason); + latch.countDown(); + } + }; + controller.addListener(callback); + + int newItemIndex = 1; + builder.setActiveQueueItemId(testQueue.get(newItemIndex).getQueueId()); + session.setPlaybackState(builder.build()); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(positionDiscontinuityReasonRef.get()).isEqualTo(Player.DISCONTINUITY_REASON_SEEK); + assertThat(oldPositionRef.get().windowIndex).isEqualTo(oldItemIndex); + assertThat(newPositionRef.get().windowIndex).isEqualTo(newItemIndex); + } + + @Test + public void + setPlaybackState_withAdjacentQueueItemAfterPlaybackDone_notifiesPositionDiscontinuityByTransition() + throws Exception { + long testDuration = 3000; + List testQueue = MediaTestUtils.createQueueItems(/* size= */ 2); + session.setQueue(testQueue); + session.setMetadata( + new MediaMetadataCompat.Builder() + .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, testDuration) + .build()); + + PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder(); + + // Set the current active queue item to index 'oldItemIndex'. + int oldItemIndex = 0; + builder.setActiveQueueItemId(testQueue.get(oldItemIndex).getQueueId()); + builder.setState( + PlaybackStateCompat.STATE_PLAYING, /* position= */ testDuration, /* playbackSpeed= */ 1.0f); + session.setPlaybackState(builder.build()); + + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference oldPositionRef = new AtomicReference<>(); + AtomicReference newPositionRef = new AtomicReference<>(); + AtomicInteger positionDiscontinuityReasonRef = new AtomicInteger(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPositionDiscontinuity( + PositionInfo oldPosition, PositionInfo newPosition, @DiscontinuityReason int reason) { + oldPositionRef.set(oldPosition); + newPositionRef.set(newPosition); + latch.countDown(); + } + }; + controller.addListener(callback); + + int newItemIndex = 1; + builder.setActiveQueueItemId(testQueue.get(newItemIndex).getQueueId()); + builder.setState( + PlaybackStateCompat.STATE_PLAYING, /* position= */ 0, /* playbackSpeed= */ 1.0f); + session.setPlaybackState(builder.build()); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(positionDiscontinuityReasonRef.get()) + .isEqualTo(Player.DISCONTINUITY_REASON_AUTO_TRANSITION); + assertThat(oldPositionRef.get().windowIndex).isEqualTo(oldItemIndex); + assertThat(newPositionRef.get().windowIndex).isEqualTo(newItemIndex); + } + + @Test + public void setPlaybackState_withDistantQueueItem_notifiesPositionDiscontinuityBySeek() + throws Exception { + List testQueue = MediaTestUtils.createQueueItems(/* size= */ 3); + session.setQueue(testQueue); + session.setMetadata(new MediaMetadataCompat.Builder().build()); + + PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder(); + + // Set the current active queue item to index 'oldItemIndex'. + int oldItemIndex = 0; + builder.setActiveQueueItemId(testQueue.get(oldItemIndex).getQueueId()); + session.setPlaybackState(builder.build()); + + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference oldPositionRef = new AtomicReference<>(); + AtomicReference newPositionRef = new AtomicReference<>(); + AtomicInteger positionDiscontinuityReasonRef = new AtomicInteger(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPositionDiscontinuity( + PositionInfo oldPosition, PositionInfo newPosition, @DiscontinuityReason int reason) { + oldPositionRef.set(oldPosition); + newPositionRef.set(newPosition); + positionDiscontinuityReasonRef.set(reason); + latch.countDown(); + } + }; + controller.addListener(callback); + + int newItemIndex = 2; + builder.setActiveQueueItemId(testQueue.get(newItemIndex).getQueueId()); + session.setPlaybackState(builder.build()); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(positionDiscontinuityReasonRef.get()).isEqualTo(Player.DISCONTINUITY_REASON_SEEK); + assertThat(oldPositionRef.get().windowIndex).isEqualTo(oldItemIndex); + assertThat(newPositionRef.get().windowIndex).isEqualTo(newItemIndex); + } + + @Test + public void setPlaybackState_withNewPosition_notifiesOnPositionDiscontinuity() throws Exception { + long testOldCurrentPositionMs = 300L; + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setState( + PlaybackStateCompat.STATE_PAUSED, testOldCurrentPositionMs, /* playbackSpeed= */ 1f) + .build()); + session.setMetadata(new MediaMetadataCompat.Builder().build()); + + AtomicReference oldPositionRef = new AtomicReference<>(); + AtomicReference newPositionRef = new AtomicReference<>(); + AtomicInteger positionDiscontinuityReasonRef = new AtomicInteger(); + CountDownLatch latch = new CountDownLatch(1); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPositionDiscontinuity( + PositionInfo oldPosition, PositionInfo newPosition, @DiscontinuityReason int reason) { + oldPositionRef.set(oldPosition); + newPositionRef.set(newPosition); + positionDiscontinuityReasonRef.set(reason); + latch.countDown(); + } + }; + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + controller.addListener(callback); + + long testNewCurrentPositionMs = 900L; + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setState( + PlaybackStateCompat.STATE_PAUSED, testNewCurrentPositionMs, /* playbackSpeed= */ 1f) + .build()); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(positionDiscontinuityReasonRef.get()).isEqualTo(Player.DISCONTINUITY_REASON_SEEK); + assertThat(oldPositionRef.get().positionMs).isEqualTo(testOldCurrentPositionMs); + assertThat(newPositionRef.get().positionMs).isEqualTo(testNewCurrentPositionMs); + } + + @Test + public void setPlaybackState_fromStateBufferingToPlaying_notifiesReadyState() throws Exception { + List testPlaylist = MediaTestUtils.createConvergedMediaItems(/* size= */ 1); + MediaMetadataCompat metadata = + MediaUtils.convertToMediaMetadataCompat(testPlaylist.get(0), /* durationMs= */ 1_000); + long testBufferedPosition = 500; + session.setMetadata(metadata); + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setState( + PlaybackStateCompat.STATE_BUFFERING, /* position= */ 0, /* playbackSpeed= */ 1f) + .setBufferedPosition(0) + .build()); + + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger stateFromParamRef = new AtomicInteger(); + AtomicInteger stateFromGetterRef = new AtomicInteger(); + AtomicLong bufferedPositionFromGetterRef = new AtomicLong(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPlaybackStateChanged(@State int state) { + stateFromParamRef.set(state); + stateFromGetterRef.set(controller.getPlaybackState()); + bufferedPositionFromGetterRef.set(controller.getBufferedPosition()); + latch.countDown(); + } + }; + controller.addListener(callback); + + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setState(PlaybackStateCompat.STATE_PLAYING, /* position= */ 0, /* playbackSpeed= */ 1f) + .setBufferedPosition(testBufferedPosition) + .build()); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(stateFromParamRef.get()).isEqualTo(STATE_READY); + assertThat(stateFromGetterRef.get()).isEqualTo(STATE_READY); + assertThat(bufferedPositionFromGetterRef.get()).isEqualTo(testBufferedPosition); + } + + @Test + public void setPlaybackState_fromStatePlayingToBuffering_notifiesBufferingState() + throws Exception { + List testPlaylist = MediaTestUtils.createConvergedMediaItems(1); + MediaMetadataCompat metadata = + MediaUtils.convertToMediaMetadataCompat(testPlaylist.get(0), /* durationMs= */ 1_000); + long testBufferingPosition = 0; + session.setMetadata(metadata); + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setState( + PlaybackStateCompat.STATE_PLAYING, /* position= */ 100, /* playbackSpeed= */ 1f) + .setBufferedPosition(500) + .build()); + + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger stateFromParamRef = new AtomicInteger(); + AtomicInteger stateFromGetterRef = new AtomicInteger(); + AtomicLong bufferedPositionFromGetterRef = new AtomicLong(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPlaybackStateChanged(int state) { + stateFromParamRef.set(state); + stateFromGetterRef.set(controller.getPlaybackState()); + bufferedPositionFromGetterRef.set(controller.getBufferedPosition()); + latch.countDown(); + } + }; + controller.addListener(callback); + + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setState( + PlaybackStateCompat.STATE_BUFFERING, /* position= */ 0, /* playbackSpeed= */ 1f) + .setBufferedPosition(testBufferingPosition) + .build()); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(stateFromParamRef.get()).isEqualTo(STATE_BUFFERING); + assertThat(stateFromGetterRef.get()).isEqualTo(STATE_BUFFERING); + assertThat(bufferedPositionFromGetterRef.get()).isEqualTo(testBufferingPosition); + } + + @Test + public void setPlaybackState_fromStateNoneToPlaying_notifiesReadyState() throws Exception { + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setState(PlaybackStateCompat.STATE_NONE, /* position= */ 0, /* playbackSpeed= */ 1f) + .build()); + + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger stateFromParamRef = new AtomicInteger(); + AtomicInteger stateFromGetterRef = new AtomicInteger(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPlaybackStateChanged(int state) { + stateFromParamRef.set(state); + stateFromGetterRef.set(controller.getPlaybackState()); + latch.countDown(); + } + }; + controller.addListener(callback); + + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setState(PlaybackStateCompat.STATE_PLAYING, /* position= */ 0, /* playbackSpeed= */ 1f) + .build()); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(stateFromParamRef.get()).isEqualTo(STATE_READY); + assertThat(stateFromGetterRef.get()).isEqualTo(STATE_READY); + } + + @Test + public void setPlaybackState_fromStatePausedToPlaying_notifiesPlayWhenReady() throws Exception { + boolean testPlayWhenReady = true; + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setState(PlaybackStateCompat.STATE_PAUSED, /* position= */ 0, /* playbackSpeed= */ 1f) + .build()); + + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean playWhenReadyFromParamRef = new AtomicBoolean(); + AtomicBoolean playWhenReadyFromGetterRef = new AtomicBoolean(); + AtomicInteger playbackSuppressionReasonFromParamRef = new AtomicInteger(); + AtomicInteger playbackSuppressionReasonFromGetterRef = new AtomicInteger(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPlayWhenReadyChanged( + boolean playWhenReady, @Player.PlaybackSuppressionReason int reason) { + playWhenReadyFromParamRef.set(playWhenReady); + playWhenReadyFromGetterRef.set(controller.getPlayWhenReady()); + playbackSuppressionReasonFromParamRef.set(reason); + playbackSuppressionReasonFromGetterRef.set(controller.getPlaybackSuppressionReason()); + latch.countDown(); + } + }; + controller.addListener(callback); + + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setState(PlaybackStateCompat.STATE_PLAYING, /* position= */ 0, /* playbackSpeed= */ 1f) + .build()); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(playWhenReadyFromParamRef.get()).isEqualTo(testPlayWhenReady); + assertThat(playWhenReadyFromGetterRef.get()).isEqualTo(testPlayWhenReady); + assertThat(playbackSuppressionReasonFromParamRef.get()) + .isEqualTo(Player.PLAYBACK_SUPPRESSION_REASON_NONE); + assertThat(playbackSuppressionReasonFromGetterRef.get()) + .isEqualTo(Player.PLAYBACK_SUPPRESSION_REASON_NONE); + } + + @Test + public void setPlaybackState_toBuffering_notifiesPlaybackStateBuffering() throws Exception { + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setState(PlaybackStateCompat.STATE_PAUSED, /* position= */ 0, /* playbackSpeed= */ 1f) + .build()); + session.setMetadata( + new MediaMetadataCompat.Builder().putLong(METADATA_KEY_DURATION, 1_000).build()); + + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger playbackStateFromParamRef = new AtomicInteger(); + AtomicInteger playbackStateFromGetterRef = new AtomicInteger(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPlaybackStateChanged(@Player.State int state) { + playbackStateFromParamRef.set(state); + playbackStateFromGetterRef.set(controller.getPlaybackState()); + latch.countDown(); + } + }; + controller.addListener(callback); + + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setState( + PlaybackStateCompat.STATE_BUFFERING, /* position= */ 0, /* playbackSpeed= */ 1f) + .build()); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(playbackStateFromParamRef.get()).isEqualTo(Player.STATE_BUFFERING); + assertThat(playbackStateFromGetterRef.get()).isEqualTo(Player.STATE_BUFFERING); + } + + @Test + public void setPlaybackState_toPausedWithEndPosition_notifiesPlaybackStateEnded() + throws Exception { + long testDuration = 1_000; + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setState(PlaybackStateCompat.STATE_PLAYING, /* position= */ 0, /* playbackSpeed= */ 1f) + .build()); + session.setMetadata( + new MediaMetadataCompat.Builder().putLong(METADATA_KEY_DURATION, testDuration).build()); + + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger playbackStateFromParamRef = new AtomicInteger(); + AtomicInteger playbackStateFromGetterRef = new AtomicInteger(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPlaybackStateChanged(@Player.State int state) { + playbackStateFromParamRef.set(state); + playbackStateFromGetterRef.set(controller.getPlaybackState()); + latch.countDown(); + } + }; + controller.addListener(callback); + + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setState( + PlaybackStateCompat.STATE_PAUSED, + /* position= */ testDuration, + /* playbackSpeed= */ 1f) + .build()); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(playbackStateFromParamRef.get()).isEqualTo(Player.STATE_ENDED); + assertThat(playbackStateFromGetterRef.get()).isEqualTo(Player.STATE_ENDED); + } + + @Test + public void setPlaybackState_withSpeed_notifiesOnPlaybackParametersChanged() throws Exception { + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference playbackParametersFromParamRef = new AtomicReference<>(); + AtomicReference playbackParametersFromGetterRef = new AtomicReference<>(); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + playbackParametersFromParamRef.set(playbackParameters); + playbackParametersFromGetterRef.set(controller.getPlaybackParameters()); + latch.countDown(); + } + }; + controller.addListener(callback); + + float testSpeed = 3.0f; + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setState( + PlaybackStateCompat.STATE_PLAYING, + /* position= */ 0, + /* playbackSpeed= */ testSpeed) + .build()); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(playbackParametersFromParamRef.get().speed).isEqualTo(testSpeed); + assertThat(playbackParametersFromGetterRef.get().speed).isEqualTo(testSpeed); + } + + @Test + public void setPlaybackToRemote_notifiesDeviceInfoAndVolume() throws Exception { + int volumeControlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE; + int maxVolume = 100; + int currentVolume = 45; + + AtomicReference deviceInfoRef = new AtomicReference<>(); + CountDownLatch latchForDeviceInfo = new CountDownLatch(1); + CountDownLatch latchForDeviceVolume = new CountDownLatch(1); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onDeviceInfoChanged(@NonNull DeviceInfo deviceInfo) { + if (deviceInfo.playbackType == DeviceInfo.PLAYBACK_TYPE_REMOTE) { + deviceInfoRef.set(deviceInfo); + latchForDeviceInfo.countDown(); + } + } + + @Override + public void onDeviceVolumeChanged(int volume, boolean muted) { + if (volume == currentVolume) { + latchForDeviceVolume.countDown(); + } + } + }; + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + controller.addListener(callback); + + session.setPlaybackToRemote(volumeControlType, maxVolume, currentVolume); + + assertThat(latchForDeviceInfo.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(latchForDeviceVolume.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(deviceInfoRef.get().maxVolume).isEqualTo(maxVolume); + } + + @Test + public void setPlaybackToLocal_notifiesDeviceInfoAndVolume() throws Exception { + if (Build.VERSION.SDK_INT == 21 || Build.VERSION.SDK_INT == 22) { + // In API 21 and 22, onAudioInfoChanged is not called. + return; + } + session.setPlaybackToRemote( + VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE, + /* maxVolume= */ 100, + /* currentVolume= */ 45); + + int testLocalStreamType = AudioManager.STREAM_ALARM; + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + int maxVolume = audioManager.getStreamMaxVolume(testLocalStreamType); + int currentVolume = audioManager.getStreamVolume(testLocalStreamType); + + AtomicReference deviceInfoRef = new AtomicReference<>(); + CountDownLatch latchForDeviceInfo = new CountDownLatch(1); + CountDownLatch latchForDeviceVolume = new CountDownLatch(1); + SessionPlayer.PlayerCallback callback = + new SessionPlayer.PlayerCallback() { + @Override + public void onDeviceInfoChanged(@NonNull DeviceInfo deviceInfo) { + if (deviceInfo.playbackType == DeviceInfo.PLAYBACK_TYPE_LOCAL) { + deviceInfoRef.set(deviceInfo); + latchForDeviceInfo.countDown(); + } + } + + @Override + public void onDeviceVolumeChanged(int volume, boolean muted) { + if (volume == currentVolume) { + latchForDeviceVolume.countDown(); + } + } + }; + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + controller.addListener(callback); + + session.setPlaybackToLocal(testLocalStreamType); + + assertThat(latchForDeviceInfo.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(latchForDeviceVolume.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(deviceInfoRef.get().maxVolume).isEqualTo(maxVolume); + } + + @Test + public void sendSessionEvent_notifiesCustomCommand() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + AtomicReference commandRef = new AtomicReference<>(); + AtomicReference argsRef = new AtomicReference<>(); + ControllerCallback callback = + new ControllerCallback() { + @Override + @NonNull + public ListenableFuture onCustomCommand( + @NonNull MediaController controller, @NonNull SessionCommand command, Bundle args) { + commandRef.set(command); + argsRef.set(args); + latch.countDown(); + return new SessionResult(RESULT_SUCCESS).asFuture(); + } + }; + controllerTestRule.createController( + session.getSessionToken(), /* waitForConnect= */ true, callback); + + String event = "customCommand"; + Bundle extras = TestUtils.createTestBundle(); + session.sendSessionEvent(event, extras); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(commandRef.get().customAction).isEqualTo(event); + assertThat(TestUtils.equals(extras, argsRef.get())).isTrue(); + } + + @Test + public void setPlaybackState_withCustomAction_notifiesCustomLayout() throws Exception { + CustomAction testCustomAction1 = + new CustomAction.Builder("testCustomAction1", "testName1", 1).build(); + CustomAction testCustomAction2 = + new CustomAction.Builder("testCustomAction2", "testName2", 2).build(); + CountDownLatch latch = new CountDownLatch(2); + ControllerCallback callback = + new ControllerCallback() { + @Override + @NonNull + public ListenableFuture onSetCustomLayout( + @NonNull MediaController controller, @NonNull List layout) { + assertThat(layout).hasSize(1); + CommandButton button = layout.get(0); + + switch ((int) latch.getCount()) { + case 2: + assertThat(button.sessionCommand.customAction) + .isEqualTo(testCustomAction1.getAction()); + assertThat(button.displayName.toString()) + .isEqualTo(testCustomAction1.getName().toString()); + assertThat(button.iconResId).isEqualTo(testCustomAction1.getIcon()); + break; + case 1: + assertThat(button.sessionCommand.customAction) + .isEqualTo(testCustomAction2.getAction()); + assertThat(button.displayName.toString()) + .isEqualTo(testCustomAction2.getName().toString()); + assertThat(button.iconResId).isEqualTo(testCustomAction2.getIcon()); + break; + } + latch.countDown(); + return new SessionResult(RESULT_SUCCESS).asFuture(); + } + }; + session.setPlaybackState( + new PlaybackStateCompat.Builder().addCustomAction(testCustomAction1).build()); + // onSetCustomLayout will be called when its connected + controllerTestRule.createController( + session.getSessionToken(), /* waitForConnect= */ true, callback); + // onSetCustomLayout will be called again when the custom action in the playback state is + // changed. + session.setPlaybackState( + new PlaybackStateCompat.Builder().addCustomAction(testCustomAction2).build()); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void setPlaybackState_withCustomAction_notifiesAvailableCommands() throws Exception { + CustomAction testCustomAction1 = + new CustomAction.Builder("testCustomAction1", "testName1", 1).build(); + CustomAction testCustomAction2 = + new CustomAction.Builder("testCustomAction2", "testName2", 2).build(); + CountDownLatch latch = new CountDownLatch(1); + ControllerCallback callback = + new ControllerCallback() { + @Override + public void onAvailableSessionCommandsChanged( + MediaController controller, SessionCommands commands) { + assertThat( + commands.contains( + new SessionCommand( + testCustomAction1.getAction(), testCustomAction1.getExtras()))) + .isFalse(); + assertThat( + commands.contains( + new SessionCommand( + testCustomAction2.getAction(), testCustomAction2.getExtras()))) + .isTrue(); + latch.countDown(); + } + }; + session.setPlaybackState( + new PlaybackStateCompat.Builder().addCustomAction(testCustomAction1).build()); + controllerTestRule.createController( + session.getSessionToken(), /* waitForConnect= */ true, callback); + session.setPlaybackState( + new PlaybackStateCompat.Builder().addCustomAction(testCustomAction2).build()); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void setCaptioningEnabled_notifiesCustomCommand() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + AtomicReference commandRef = new AtomicReference<>(); + AtomicReference argsRef = new AtomicReference<>(); + ControllerCallback callback = + new ControllerCallback() { + @Override + @NonNull + public ListenableFuture onCustomCommand( + @NonNull MediaController controller, + @NonNull SessionCommand command, + @Nullable Bundle args) { + commandRef.set(command); + argsRef.set(args); + latch.countDown(); + return new SessionResult(RESULT_SUCCESS).asFuture(); + } + }; + controllerTestRule.createController(session.getSessionToken(), true, callback); + + session.setCaptioningEnabled(true); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(commandRef.get().customAction) + .isEqualTo(SESSION_COMMAND_ON_CAPTIONING_ENABLED_CHANGED); + BundleSubject.assertThat(argsRef.get()).bool(ARGUMENT_CAPTIONING_ENABLED).isTrue(); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaLibrarySessionCallbackTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaLibrarySessionCallbackTest.java new file mode 100644 index 0000000000..ad64a9def8 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaLibrarySessionCallbackTest.java @@ -0,0 +1,139 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.LibraryResult.RESULT_SUCCESS; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.TIMEOUT_MS; +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.MediumTest; +import com.google.android.exoplayer2.session.MediaLibraryService.LibraryParams; +import com.google.android.exoplayer2.session.MediaLibraryService.MediaLibrarySession; +import com.google.android.exoplayer2.session.vct.common.HandlerThreadTestRule; +import com.google.android.exoplayer2.session.vct.common.MainLooperTestRule; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.concurrent.CountDownLatch; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for {@link MediaLibrarySession.MediaLibrarySessionCallback}. + * + *

TODO: Make this class extend MediaSessionCallbackTest. TODO: Create MediaLibrarySessionTest + * which extends MediaSessionTest. + */ +@RunWith(AndroidJUnit4.class) +@MediumTest +public class MediaLibrarySessionCallbackTest { + + @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); + + @Rule + public final HandlerThreadTestRule threadTestRule = + new HandlerThreadTestRule("MediaLibrarySessionCallbackTest"); + + @Rule public final RemoteControllerTestRule controllerTestRule = new RemoteControllerTestRule(); + + @Rule public final MediaSessionTestRule sessionTestRule = new MediaSessionTestRule(); + + private Context context; + private MockPlayer player; + + @Before + public void setUp() { + context = ApplicationProvider.getApplicationContext(); + player = + new MockPlayer.Builder() + .setApplicationLooper(threadTestRule.getHandler().getLooper()) + .build(); + } + + @Test + public void onSubscribe() throws Exception { + String testParentId = "testSubscribeId"; + LibraryParams testParams = MediaTestUtils.createLibraryParams(); + + CountDownLatch latch = new CountDownLatch(1); + MediaLibrarySession.MediaLibrarySessionCallback sessionCallback = + new MediaLibrarySession.MediaLibrarySessionCallback() { + @Override + @NonNull + public ListenableFuture onSubscribe( + @NonNull MediaLibrarySession session, + @NonNull MediaSession.ControllerInfo browser, + @NonNull String parentId, + LibraryParams params) { + assertThat(parentId).isEqualTo(testParentId); + MediaTestUtils.assertLibraryParamsEquals(testParams, params); + latch.countDown(); + return new LibraryResult(RESULT_SUCCESS).asFuture(); + } + }; + + MockMediaLibraryService service = new MockMediaLibraryService(); + service.attachBaseContext(context); + + MediaLibrarySession session = + sessionTestRule.ensureReleaseAfterTest( + new MediaLibrarySession.Builder(service, player, sessionCallback) + .setId("testOnSubscribe") + .build()); + RemoteMediaBrowser browser = controllerTestRule.createRemoteBrowser(session.getToken()); + browser.subscribe(testParentId, testParams); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void onUnsubscribe() throws Exception { + String testParentId = "testUnsubscribeId"; + + CountDownLatch latch = new CountDownLatch(1); + MediaLibrarySession.MediaLibrarySessionCallback sessionCallback = + new MediaLibrarySession.MediaLibrarySessionCallback() { + @Override + @NonNull + public ListenableFuture onUnsubscribe( + @NonNull MediaLibrarySession session, + @NonNull MediaSession.ControllerInfo browser, + @NonNull String parentId) { + assertThat(parentId).isEqualTo(testParentId); + latch.countDown(); + return new LibraryResult(RESULT_SUCCESS).asFuture(); + } + }; + + MockMediaLibraryService service = new MockMediaLibraryService(); + service.attachBaseContext(context); + + MediaLibrarySession session = + sessionTestRule.ensureReleaseAfterTest( + new MediaLibrarySession.Builder(service, player, sessionCallback) + .setId("testOnUnsubscribe") + .build()); + RemoteMediaBrowser browser = controllerTestRule.createRemoteBrowser(session.getToken()); + browser.unsubscribe(testParentId); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionAndControllerTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionAndControllerTest.java new file mode 100644 index 0000000000..086006493b --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionAndControllerTest.java @@ -0,0 +1,154 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.Player.STATE_IDLE; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.LONG_TIMEOUT_MS; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.TIMEOUT_MS; +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.Context; +import android.os.Build; +import android.os.HandlerThread; +import android.os.Looper; +import androidx.annotation.NonNull; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import com.google.android.exoplayer2.session.vct.common.TestHandler; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link MediaSession} and {@link MediaController} in the same process. */ +@RunWith(AndroidJUnit4.class) +@LargeTest +public class MediaSessionAndControllerTest { + + private Context context; + private TestHandler handler; + + @Before + public void setUp() { + context = ApplicationProvider.getApplicationContext(); + + HandlerThread handlerThread = new HandlerThread("MediaSessionAndControllerTest"); + handlerThread.start(); + handler = new TestHandler(handlerThread.getLooper()); + } + + @After + public void cleanUp() { + if (Build.VERSION.SDK_INT >= 18) { + handler.getLooper().quitSafely(); + } else { + handler.getLooper().quit(); + } + } + + /** Test potential deadlock for calls between controller and session. */ + @Test + public void deadlock() throws Exception { + HandlerThread testThread = new HandlerThread("deadlock"); + testThread.start(); + TestHandler testHandler = new TestHandler(testThread.getLooper()); + AtomicReference sessionRef = new AtomicReference<>(); + AtomicReference controllerRef = new AtomicReference<>(); + try { + MockPlayer player = + new MockPlayer.Builder().setApplicationLooper(testThread.getLooper()).build(); + handler.postAndSync( + () -> + sessionRef.set(new MediaSession.Builder(context, player).setId("deadlock").build())); + controllerRef.set(createController(sessionRef.get().getToken(), testThread.getLooper())); + // This may hang if deadlock happens. + testHandler.postAndSync( + () -> { + int state = STATE_IDLE; + MediaController controller = controllerRef.get(); + for (int i = 0; i < 100; i++) { + // triggers call from session to controller. + player.notifyPlaybackStateChanged(state); + // triggers call from controller to session. + controller.play(); + + // Repeat above + player.notifyPlaybackStateChanged(state); + controller.pause(); + player.notifyPlaybackStateChanged(state); + controller.seekTo(0); + player.notifyPlaybackStateChanged(state); + controller.next(); + player.notifyPlaybackStateChanged(state); + controller.previous(); + } + }, + LONG_TIMEOUT_MS); + } finally { + testHandler.postAndSync( + () -> { + if (controllerRef.get() != null) { + controllerRef.get().release(); + controllerRef.set(null); + } + }); + + handler.postAndSync( + () -> { + // Clean up here because sessionHandler will be removed afterwards. + if (sessionRef.get() != null) { + sessionRef.get().release(); + } + }); + + if (Build.VERSION.SDK_INT >= 18) { + testThread.quitSafely(); + } else { + testThread.quit(); + } + } + } + + private MediaController createController( + @NonNull SessionToken token, @NonNull Looper applicationLooper) throws Exception { + CountDownLatch connectedLatch = new CountDownLatch(1); + AtomicReference controller = new AtomicReference<>(); + handler.postAndSync( + () -> { + // Create controller on the test handler, for changing MediaBrowserCompat's Handler + // Looper. Otherwise, MediaBrowserCompat will post all the commands to the handler + // and commands wouldn't be run if tests codes waits on the test handler. + MediaController.Builder builder = + new MediaController.Builder(context) + .setSessionToken(token) + .setApplicationLooper(applicationLooper) + .setControllerCallback( + new MediaController.ControllerCallback() { + @Override + public void onConnected(MediaController controller) { + connectedLatch.countDown(); + } + }); + controller.set(builder.build()); + }); + assertThat(connectedLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + return controller.get(); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionCallbackTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionCallbackTest.java new file mode 100644 index 0000000000..8b847aaa13 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionCallbackTest.java @@ -0,0 +1,349 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.SessionResult.RESULT_ERROR_INVALID_STATE; +import static com.google.android.exoplayer2.session.SessionResult.RESULT_SUCCESS; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.NO_RESPONSE_TIMEOUT_MS; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.TIMEOUT_MS; +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Rating; +import com.google.android.exoplayer2.StarRating; +import com.google.android.exoplayer2.session.MediaSession.ControllerInfo; +import com.google.android.exoplayer2.session.vct.common.HandlerThreadTestRule; +import com.google.android.exoplayer2.session.vct.common.MainLooperTestRule; +import com.google.android.exoplayer2.session.vct.common.TestUtils; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.ArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link MediaSession.SessionCallback}. */ +@RunWith(AndroidJUnit4.class) +@LargeTest +public class MediaSessionCallbackTest { + private static final String TAG = "MediaSessionCallbackTest"; + + @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); + + @Rule public final HandlerThreadTestRule threadTestRule = new HandlerThreadTestRule(TAG); + + @Rule public final RemoteControllerTestRule controllerTestRule = new RemoteControllerTestRule(); + + @Rule public final MediaSessionTestRule sessionTestRule = new MediaSessionTestRule(); + + private Context context; + private MockPlayer player; + + @Before + public void setUp() { + context = ApplicationProvider.getApplicationContext(); + player = + new MockPlayer.Builder() + .setLatchCount(1) + .setApplicationLooper(threadTestRule.getHandler().getLooper()) + .build(); + } + + @Test + public void onPostConnect_afterConnected() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + MediaSession.SessionCallback callback = + new MediaSession.SessionCallback() { + @Override + public void onPostConnect( + @NonNull MediaSession session, @NonNull ControllerInfo controller) { + latch.countDown(); + } + }; + MediaSession session = + sessionTestRule.ensureReleaseAfterTest( + new MediaSession.Builder(context, player) + .setSessionCallback(callback) + .setId("testOnPostConnect_afterConnected") + .build()); + controllerTestRule.createRemoteController(session.getToken()); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void onPostConnect_afterConnectionRejected() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + MediaSession.SessionCallback callback = + new MediaSession.SessionCallback() { + @Nullable + @Override + public MediaSession.ConnectResult onConnect( + MediaSession session, ControllerInfo controller) { + return null; + } + + @Override + public void onPostConnect( + @NonNull MediaSession session, @NonNull ControllerInfo controller) { + latch.countDown(); + } + }; + MediaSession session = + sessionTestRule.ensureReleaseAfterTest( + new MediaSession.Builder(context, player) + .setSessionCallback(callback) + .setId("testOnPostConnect_afterConnectionRejected") + .build()); + controllerTestRule.createRemoteController(session.getToken()); + assertThat(latch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); + } + + @Test + public void onCommandRequest() throws Exception { + MockOnCommandCallback callback = new MockOnCommandCallback(); + MediaSession session = + sessionTestRule.ensureReleaseAfterTest( + new MediaSession.Builder(context, player) + .setSessionCallback(callback) + .setId("testOnCommandRequest") + .build()); + RemoteMediaController controller = + controllerTestRule.createRemoteController(session.getToken()); + + controller.prepare(); + assertThat(player.countDownLatch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); + assertThat(player.prepareCalled).isFalse(); + assertThat(callback.commands).hasSize(1); + assertThat(callback.commands.get(0)).isEqualTo(Player.COMMAND_PREPARE_STOP); + + controller.play(); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.playCalled).isTrue(); + assertThat(player.prepareCalled).isFalse(); + assertThat(callback.commands).hasSize(2); + assertThat(callback.commands.get(1)).isEqualTo(Player.COMMAND_PLAY_PAUSE); + } + + @Test + public void onCustomCommand() throws Exception { + // TODO(jaewan): Need to revisit with the permission. + SessionCommand testCommand = new SessionCommand("testCustomCommand", null); + Bundle testArgs = new Bundle(); + testArgs.putString("args", "testOnCustomCommand"); + + CountDownLatch latch = new CountDownLatch(1); + MediaSession.SessionCallback callback = + new MediaSession.SessionCallback() { + @Nullable + @Override + public MediaSession.ConnectResult onConnect( + MediaSession session, ControllerInfo controller) { + SessionCommands commands = + new SessionCommands.Builder() + .addAllPredefinedCommands(SessionCommand.COMMAND_VERSION_1) + .add(testCommand) + .build(); + return new MediaSession.ConnectResult(commands, Player.Commands.EMPTY); + } + + @Override + @NonNull + public ListenableFuture onCustomCommand( + @NonNull MediaSession session, + @NonNull MediaSession.ControllerInfo controller, + @NonNull SessionCommand sessionCommand, + Bundle args) { + assertThat(controller.getPackageName()).isEqualTo(SUPPORT_APP_PACKAGE_NAME); + assertThat(sessionCommand).isEqualTo(testCommand); + assertThat(TestUtils.equals(testArgs, args)).isTrue(); + latch.countDown(); + return new SessionResult(RESULT_SUCCESS).asFuture(); + } + }; + + MediaSession session = + sessionTestRule.ensureReleaseAfterTest( + new MediaSession.Builder(context, player) + .setSessionCallback(callback) + .setId("testOnCustomCommand") + .build()); + RemoteMediaController controller = + controllerTestRule.createRemoteController(session.getToken()); + SessionResult result = controller.sendCustomCommand(testCommand, testArgs); + assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void onSetMediaUri() throws Exception { + Uri testUri = Uri.parse("foo://boo"); + Bundle testExtras = TestUtils.createTestBundle(); + CountDownLatch latch = new CountDownLatch(1); + MediaSession.SessionCallback callback = + new MediaSession.SessionCallback() { + @Override + public int onSetMediaUri( + @NonNull MediaSession session, + @NonNull ControllerInfo controller, + @NonNull Uri uri, + @Nullable Bundle extras) { + assertThat(controller.getPackageName()).isEqualTo(SUPPORT_APP_PACKAGE_NAME); + assertThat(uri).isEqualTo(testUri); + assertThat(TestUtils.equals(testExtras, extras)).isTrue(); + latch.countDown(); + return RESULT_SUCCESS; + } + }; + MediaSession session = + sessionTestRule.ensureReleaseAfterTest( + new MediaSession.Builder(context, player) + .setSessionCallback(callback) + .setId("testOnSetMediaUri") + .build()); + RemoteMediaController controller = + controllerTestRule.createRemoteController(session.getToken()); + + controller.setMediaUri(testUri, testExtras); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void onSetRating() throws Exception { + float ratingValue = 3.5f; + Rating testRating = new StarRating(5, ratingValue); + String testMediaId = "media_id"; + + CountDownLatch latch = new CountDownLatch(1); + MediaSession.SessionCallback callback = + new MediaSession.SessionCallback() { + @Override + @NonNull + public ListenableFuture onSetRating( + @NonNull MediaSession session, + @NonNull ControllerInfo controller, + @NonNull String mediaId, + @NonNull Rating rating) { + assertThat(controller.getPackageName()).isEqualTo(SUPPORT_APP_PACKAGE_NAME); + assertThat(mediaId).isEqualTo(testMediaId); + assertThat(rating).isEqualTo(testRating); + latch.countDown(); + return new SessionResult(RESULT_SUCCESS).asFuture(); + } + }; + + MediaSession session = + sessionTestRule.ensureReleaseAfterTest( + new MediaSession.Builder(context, player) + .setSessionCallback(callback) + .setId("testOnSetRating") + .build()); + RemoteMediaController controller = + controllerTestRule.createRemoteController(session.getToken()); + SessionResult result = controller.setRating(testMediaId, testRating); + assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void onConnect() throws Exception { + AtomicReference connectionHints = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + MediaSession session = + sessionTestRule.ensureReleaseAfterTest( + new MediaSession.Builder(context, player) + .setId("testOnConnect") + .setSessionCallback( + new MediaSession.SessionCallback() { + @Nullable + @Override + public MediaSession.ConnectResult onConnect( + MediaSession session, ControllerInfo controller) { + // TODO: Get uid of client app's and compare. + if (!SUPPORT_APP_PACKAGE_NAME.equals(controller.getPackageName())) { + return null; + } + connectionHints.set(controller.getConnectionHints()); + latch.countDown(); + return super.onConnect(session, controller); + } + }) + .build()); + Bundle testConnectionHints = new Bundle(); + testConnectionHints.putString("test_key", "test_value"); + + controllerTestRule.createRemoteController( + session.getToken(), /* waitForConnection= */ false, testConnectionHints); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(TestUtils.equals(testConnectionHints, connectionHints.get())).isTrue(); + } + + @Test + public void onDisconnected() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + MediaSession session = + sessionTestRule.ensureReleaseAfterTest( + new MediaSession.Builder(context, player) + .setId("testOnDisconnected") + .setSessionCallback( + new MediaSession.SessionCallback() { + @Override + public void onDisconnected( + @NonNull MediaSession session, @NonNull ControllerInfo controller) { + assertThat(controller.getPackageName()).isEqualTo(SUPPORT_APP_PACKAGE_NAME); + // TODO: Get uid of client app's and compare. + latch.countDown(); + } + }) + .build()); + RemoteMediaController controller = + controllerTestRule.createRemoteController(session.getToken()); + controller.release(); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + static class MockOnCommandCallback extends MediaSession.SessionCallback { + public final ArrayList commands = new ArrayList<>(); + + @Override + public int onPlayerCommandRequest( + @NonNull MediaSession session, + @NonNull ControllerInfo controllerInfo, + @Player.Command int command) { + // TODO: Get uid of client app's and compare. + assertThat(controllerInfo.getPackageName()).isEqualTo(SUPPORT_APP_PACKAGE_NAME); + assertThat(controllerInfo.isTrusted()).isFalse(); + commands.add(command); + if (command == Player.COMMAND_PREPARE_STOP) { + return RESULT_ERROR_INVALID_STATE; + } + return RESULT_SUCCESS; + } + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionCallbackWithMediaControllerCompatTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionCallbackWithMediaControllerCompatTest.java new file mode 100644 index 0000000000..e37f9bbdc1 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionCallbackWithMediaControllerCompatTest.java @@ -0,0 +1,1116 @@ +/* + * 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.google.android.exoplayer2.session; + +import static androidx.media.MediaSessionManager.RemoteUserInfo.LEGACY_CONTROLLER; +import static com.google.android.exoplayer2.Player.COMMAND_PLAY_PAUSE; +import static com.google.android.exoplayer2.Player.COMMAND_PREPARE_STOP; +import static com.google.android.exoplayer2.Player.STATE_IDLE; +import static com.google.android.exoplayer2.session.SessionResult.RESULT_ERROR_INVALID_STATE; +import static com.google.android.exoplayer2.session.SessionResult.RESULT_SUCCESS; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.LONG_TIMEOUT_MS; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.NO_RESPONSE_TIMEOUT_MS; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.TIMEOUT_MS; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.VOLUME_CHANGE_TIMEOUT_MS; +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.Context; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.media.MediaDescriptionCompat; +import android.support.v4.media.RatingCompat; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.MediaSessionCompat.QueueItem; +import android.support.v4.media.session.PlaybackStateCompat; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.media.AudioAttributesCompat; +import androidx.media.AudioManagerCompat; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Rating; +import com.google.android.exoplayer2.audio.AudioAttributes; +import com.google.android.exoplayer2.device.DeviceInfo; +import com.google.android.exoplayer2.session.MediaSession.ControllerInfo; +import com.google.android.exoplayer2.session.MediaSession.SessionCallback; +import com.google.android.exoplayer2.session.vct.common.HandlerThreadTestRule; +import com.google.android.exoplayer2.session.vct.common.MainLooperTestRule; +import com.google.android.exoplayer2.session.vct.common.PollingCheck; +import com.google.android.exoplayer2.session.vct.common.TestHandler; +import com.google.android.exoplayer2.session.vct.common.TestUtils; +import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.Util; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link SessionCallback} working with {@link MediaControllerCompat}. */ +@RunWith(AndroidJUnit4.class) +@LargeTest +public class MediaSessionCallbackWithMediaControllerCompatTest { + + private static final String TAG = "MSCallbackWithMCCTest"; + + private static final String EXPECTED_CONTROLLER_PACKAGE_NAME = + (Build.VERSION.SDK_INT < 21 || Build.VERSION.SDK_INT >= 24) + ? SUPPORT_APP_PACKAGE_NAME + : LEGACY_CONTROLLER; + + @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); + + @Rule public final HandlerThreadTestRule threadTestRule = new HandlerThreadTestRule(TAG); + + private Context context; + private TestHandler handler; + private MediaSession session; + private RemoteMediaControllerCompat controller; + private MockPlayer player; + private AudioManager audioManager; + + @Before + public void setUp() { + context = ApplicationProvider.getApplicationContext(); + handler = threadTestRule.getHandler(); + player = + new MockPlayer.Builder().setLatchCount(1).setApplicationLooper(handler.getLooper()).build(); + audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + } + + @After + public void cleanUp() { + if (session != null) { + session.release(); + session = null; + } + if (controller != null) { + controller.cleanUp(); + controller = null; + } + } + + @Test + public void onDisconnected_afterTimeout_isCalled() throws Exception { + CountDownLatch disconnectedLatch = new CountDownLatch(1); + session = + new MediaSession.Builder(context, player) + .setId("onDisconnected_afterTimeout_isCalled") + .setSessionCallback( + new SessionCallback() { + private ControllerInfo connectedController; + + @Nullable + @Override + public MediaSession.ConnectResult onConnect( + MediaSession session, ControllerInfo controller) { + if (EXPECTED_CONTROLLER_PACKAGE_NAME.equals(controller.getPackageName())) { + connectedController = controller; + return super.onConnect(session, controller); + } + return null; + } + + @Override + public void onDisconnected( + @NonNull MediaSession session, @NonNull ControllerInfo controller) { + if (Util.areEqual(connectedController, controller)) { + disconnectedLatch.countDown(); + } + } + }) + .build(); + // Make onDisconnected() to be called immediately after the connection. + session.setLegacyControllerConnectionTimeoutMs(0); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + // Invoke any command for session to recognize the controller compat. + controller.getTransportControls().seekTo(111); + assertThat(disconnectedLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void onConnected_afterDisconnectedByTimeout_isCalled() throws Exception { + CountDownLatch connectedLatch = new CountDownLatch(2); + CountDownLatch disconnectedLatch = new CountDownLatch(1); + session = + new MediaSession.Builder(context, player) + .setId("onConnected_afterDisconnectedByTimeout_isCalled") + .setSessionCallback( + new SessionCallback() { + private ControllerInfo connectedController; + + @Nullable + @Override + public MediaSession.ConnectResult onConnect( + MediaSession session, ControllerInfo controller) { + if (EXPECTED_CONTROLLER_PACKAGE_NAME.equals(controller.getPackageName())) { + connectedController = controller; + connectedLatch.countDown(); + return super.onConnect(session, controller); + } + return null; + } + + @Override + public void onDisconnected( + @NonNull MediaSession session, @NonNull ControllerInfo controller) { + if (Util.areEqual(connectedController, controller)) { + disconnectedLatch.countDown(); + } + } + }) + .build(); + // Make onDisconnected() to be called immediately after the connection. + session.setLegacyControllerConnectionTimeoutMs(0); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + // Invoke any command for session to recognize the controller compat. + controller.getTransportControls().seekTo(111); + assertThat(disconnectedLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + + // Test whenter onConnect() is called again after the onDisconnected(). + controller.getTransportControls().seekTo(111); + + assertThat(connectedLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void play() throws Exception { + session = + new MediaSession.Builder(context, player) + .setId("play") + .setSessionCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + + controller.getTransportControls().play(); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.playCalled).isTrue(); + } + + @Test + public void pause() throws Exception { + session = + new MediaSession.Builder(context, player) + .setId("pause") + .setSessionCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + + controller.getTransportControls().pause(); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.pauseCalled).isTrue(); + } + + @Test + public void stop() throws Exception { + session = + new MediaSession.Builder(context, player) + .setId("stop") + .setSessionCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + + controller.getTransportControls().stop(); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.stopCalled).isTrue(); + } + + @Test + public void prepare() throws Exception { + session = + new MediaSession.Builder(context, player) + .setId("prepare") + .setSessionCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + + controller.getTransportControls().prepare(); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.prepareCalled).isTrue(); + } + + @Test + public void seekTo() throws Exception { + session = + new MediaSession.Builder(context, player) + .setId("seekTo") + .setSessionCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + + long seekPosition = 12125L; + controller.getTransportControls().seekTo(seekPosition); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.seekToCalled).isTrue(); + assertThat(player.seekPositionMs).isEqualTo(seekPosition); + } + + @Test + public void setPlaybackSpeed_callsSetPlaybackSpeed() throws Exception { + session = + new MediaSession.Builder(context, player) + .setId("setPlaybackSpeed") + .setSessionCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + + float testSpeed = 2.0f; + controller.getTransportControls().setPlaybackSpeed(testSpeed); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.setPlaybackSpeedCalled).isTrue(); + assertThat(player.playbackParameters.speed).isEqualTo(testSpeed); + } + + @Test + public void addQueueItem() throws Exception { + session = + new MediaSession.Builder(context, player) + .setId("addQueueItem") + .setSessionCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + + handler.postAndSync( + () -> { + player.timeline = MediaTestUtils.createTimeline(/* windowCount= */ 10); + player.notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + }); + + // Prepare an item to add. + String mediaId = "newMediaItemId"; + MediaDescriptionCompat desc = new MediaDescriptionCompat.Builder().setMediaId(mediaId).build(); + controller.addQueueItem(desc); + + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.addMediaItemsCalled).isTrue(); + assertThat(player.mediaItems.get(0).mediaId).isEqualTo(mediaId); + } + + @Test + public void addQueueItemWithIndex() throws Exception { + session = + new MediaSession.Builder(context, player) + .setId("addQueueItemWithIndex") + .setSessionCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + + handler.postAndSync( + () -> { + player.timeline = MediaTestUtils.createTimeline(/* windowCount= */ 10); + player.notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + }); + + // Prepare an item to add. + int testIndex = 1; + String mediaId = "media_id"; + MediaDescriptionCompat desc = new MediaDescriptionCompat.Builder().setMediaId(mediaId).build(); + controller.addQueueItem(desc, testIndex); + + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.addMediaItemsCalled).isTrue(); + assertThat(player.index).isEqualTo(testIndex); + assertThat(player.mediaItems.get(0).mediaId).isEqualTo(mediaId); + } + + @Test + public void removeQueueItem() throws Exception { + session = + new MediaSession.Builder(context, player) + .setId("removeQueueItem") + .setSessionCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + + List mediaItems = MediaTestUtils.createConvergedMediaItems(/* size= */ 10); + handler.postAndSync( + () -> { + player.timeline = new PlaylistTimeline(mediaItems); + player.notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + }); + + // Select an item to remove. + int targetIndex = 3; + MediaItem targetItem = mediaItems.get(targetIndex); + MediaDescriptionCompat desc = + new MediaDescriptionCompat.Builder().setMediaId(targetItem.mediaId).build(); + controller.removeQueueItem(desc); + + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.removeMediaItemsCalled).isTrue(); + assertThat(player.fromIndex).isEqualTo(targetIndex); + assertThat(player.toIndex).isEqualTo(targetIndex + 1); + } + + @Test + public void skipToPrevious() throws Exception { + session = + new MediaSession.Builder(context, player) + .setId("skipToPrevious") + .setSessionCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + + controller.getTransportControls().skipToPrevious(); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.previousCalled).isTrue(); + } + + @Test + public void skipToNext() throws Exception { + session = + new MediaSession.Builder(context, player) + .setId("skipToNext") + .setSessionCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + + controller.getTransportControls().skipToNext(); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.nextCalled).isTrue(); + } + + @Test + public void skipToQueueItem() throws Exception { + session = + new MediaSession.Builder(context, player) + .setId("skipToQueueItem") + .setSessionCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + + handler.postAndSync( + () -> { + player.timeline = MediaTestUtils.createTimeline(/* windowCount= */ 10); + player.notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + }); + + // Get Queue from local MediaControllerCompat. + List queue = session.getSessionCompat().getController().getQueue(); + int targetIndex = 3; + controller.getTransportControls().skipToQueueItem(queue.get(targetIndex).getQueueId()); + + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.seekToDefaultPositionWithWindowIndexCalled).isTrue(); + assertThat(player.seekWindowIndex).isEqualTo(targetIndex); + } + + @Test + public void setShuffleMode() throws Exception { + session = + new MediaSession.Builder(context, player) + .setId("setShuffleMode") + .setSessionCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + + @PlaybackStateCompat.ShuffleMode int testShuffleMode = PlaybackStateCompat.SHUFFLE_MODE_GROUP; + controller.getTransportControls().setShuffleMode(testShuffleMode); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + + assertThat(player.setShuffleModeCalled).isTrue(); + assertThat(player.shuffleModeEnabled).isTrue(); + } + + @Test + public void setRepeatMode() throws Exception { + session = + new MediaSession.Builder(context, player) + .setId("setRepeatMode") + .setSessionCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + + int testRepeatMode = Player.REPEAT_MODE_ALL; + controller.getTransportControls().setRepeatMode(testRepeatMode); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + + assertThat(player.setRepeatModeCalled).isTrue(); + assertThat(player.repeatMode).isEqualTo(testRepeatMode); + } + + @Test + public void setVolumeTo_setsDeviceVolume() throws Exception { + session = + new MediaSession.Builder(context, player) + .setId("setVolumeTo_setsDeviceVolume") + .setSessionCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + MockPlayer remotePlayer = + new MockPlayer.Builder().setLatchCount(1).setApplicationLooper(handler.getLooper()).build(); + handler.postAndSync( + () -> { + remotePlayer.deviceInfo = + new DeviceInfo( + DeviceInfo.PLAYBACK_TYPE_REMOTE, /* minVolume= */ 0, /* maxVolume= */ 100); + remotePlayer.deviceVolume = 23; + session.setPlayer(remotePlayer); + }); + + int targetVolume = 50; + controller.setVolumeTo(targetVolume, /* flags= */ 0); + + assertThat(remotePlayer.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(remotePlayer.setDeviceVolumeCalled).isTrue(); + assertThat(remotePlayer.deviceVolume).isEqualTo(targetVolume); + } + + @Test + public void adjustVolume_raise_increasesDeviceVolume() throws Exception { + session = + new MediaSession.Builder(context, player) + .setId("adjustVolume_raise_increasesDeviceVolume") + .setSessionCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + MockPlayer remotePlayer = + new MockPlayer.Builder().setLatchCount(1).setApplicationLooper(handler.getLooper()).build(); + handler.postAndSync( + () -> { + remotePlayer.deviceInfo = + new DeviceInfo( + DeviceInfo.PLAYBACK_TYPE_REMOTE, /* minVolume= */ 0, /* maxVolume= */ 100); + remotePlayer.deviceVolume = 23; + session.setPlayer(remotePlayer); + }); + + controller.adjustVolume(AudioManager.ADJUST_RAISE, /* flags= */ 0); + + assertThat(remotePlayer.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(remotePlayer.increaseDeviceVolumeCalled).isTrue(); + } + + @Test + public void adjustVolume_lower_decreasesDeviceVolume() throws Exception { + session = + new MediaSession.Builder(context, player) + .setId("adjustVolume_lower_decreasesDeviceVolume") + .setSessionCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + MockPlayer remotePlayer = + new MockPlayer.Builder().setLatchCount(1).setApplicationLooper(handler.getLooper()).build(); + handler.postAndSync( + () -> { + remotePlayer.deviceInfo = + new DeviceInfo( + DeviceInfo.PLAYBACK_TYPE_REMOTE, /* minVolume= */ 0, /* maxVolume= */ 100); + remotePlayer.deviceVolume = 23; + session.setPlayer(remotePlayer); + }); + + controller.adjustVolume(AudioManager.ADJUST_LOWER, /* flags= */ 0); + + assertThat(remotePlayer.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(remotePlayer.decreaseDeviceVolumeCalled).isTrue(); + } + + @Test + public void setVolumeWithLocalVolume() throws Exception { + if (Build.VERSION.SDK_INT >= 21 && audioManager.isVolumeFixed()) { + // This test is not eligible for this device. + return; + } + + session = + new MediaSession.Builder(context, player) + .setId("setVolumeWithLocalVolume") + .setSessionCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + + // Here, we intentionally choose STREAM_ALARM in order not to consider + // 'Do Not Disturb' or 'Volume limit'. + int stream = AudioManager.STREAM_ALARM; + int maxVolume = AudioManagerCompat.getStreamMaxVolume(audioManager, stream); + int minVolume = AudioManagerCompat.getStreamMinVolume(audioManager, stream); + Log.d(TAG, "maxVolume=" + maxVolume + ", minVolume=" + minVolume); + if (maxVolume <= minVolume) { + return; + } + + handler.postAndSync( + () -> { + // Set stream of the session. + AudioAttributes attrs = + MediaUtils.convertToAudioAttributes( + new AudioAttributesCompat.Builder().setLegacyStreamType(stream).build()); + player.audioAttributes = attrs; + player.notifyAudioAttributesChanged(attrs); + }); + + int originalVolume = audioManager.getStreamVolume(stream); + int targetVolume = originalVolume == minVolume ? originalVolume + 1 : originalVolume - 1; + Log.d(TAG, "originalVolume=" + originalVolume + ", targetVolume=" + targetVolume); + + controller.setVolumeTo(targetVolume, AudioManager.FLAG_SHOW_UI); + PollingCheck.waitFor( + VOLUME_CHANGE_TIMEOUT_MS, () -> targetVolume == audioManager.getStreamVolume(stream)); + + // Set back to original volume. + audioManager.setStreamVolume(stream, originalVolume, /* flags= */ 0); + } + + @Test + public void adjustVolumeWithLocalVolume() throws Exception { + if (Build.VERSION.SDK_INT >= 21 && audioManager.isVolumeFixed()) { + // This test is not eligible for this device. + return; + } + + session = + new MediaSession.Builder(context, player) + .setId("adjustVolumeWithLocalVolume") + .setSessionCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + + // Here, we intentionally choose STREAM_ALARM in order not to consider + // 'Do Not Disturb' or 'Volume limit'. + int stream = AudioManager.STREAM_ALARM; + int maxVolume = AudioManagerCompat.getStreamMaxVolume(audioManager, stream); + int minVolume = AudioManagerCompat.getStreamMinVolume(audioManager, stream); + Log.d(TAG, "maxVolume=" + maxVolume + ", minVolume=" + minVolume); + if (maxVolume <= minVolume) { + return; + } + + handler.postAndSync( + () -> { + // Set stream of the session. + AudioAttributes attrs = + MediaUtils.convertToAudioAttributes( + new AudioAttributesCompat.Builder().setLegacyStreamType(stream).build()); + player.audioAttributes = attrs; + player.notifyAudioAttributesChanged(attrs); + }); + + int originalVolume = audioManager.getStreamVolume(stream); + int direction = + originalVolume == minVolume ? AudioManager.ADJUST_RAISE : AudioManager.ADJUST_LOWER; + int targetVolume = originalVolume + direction; + Log.d(TAG, "originalVolume=" + originalVolume + ", targetVolume=" + targetVolume); + + controller.adjustVolume(direction, AudioManager.FLAG_SHOW_UI); + PollingCheck.waitFor( + VOLUME_CHANGE_TIMEOUT_MS, () -> targetVolume == audioManager.getStreamVolume(stream)); + + // Set back to original volume. + audioManager.setStreamVolume(stream, originalVolume, /* flags= */ 0); + } + + @Test + public void sendCommand() throws Exception { + // TODO(jaewan): Need to revisit with the permission. + String testCommand = "test_command"; + Bundle testArgs = new Bundle(); + testArgs.putString("args", "test_args"); + + CountDownLatch latch = new CountDownLatch(1); + SessionCallback callback = + new SessionCallback() { + @Nullable + @Override + public MediaSession.ConnectResult onConnect( + MediaSession session, ControllerInfo controller) { + if (EXPECTED_CONTROLLER_PACKAGE_NAME.equals(controller.getPackageName())) { + MediaSession.ConnectResult commands = super.onConnect(session, controller); + SessionCommands.Builder builder = + new SessionCommands.Builder(commands.availableSessionCommands); + builder.add(new SessionCommand(testCommand, /* extras= */ null)); + return new MediaSession.ConnectResult( + builder.build(), commands.availablePlayerCommands); + } else { + return null; + } + } + + @Override + @NonNull + public ListenableFuture onCustomCommand( + @NonNull MediaSession session, + @NonNull ControllerInfo controller, + @NonNull SessionCommand sessionCommand, + Bundle args) { + assertThat(sessionCommand.customAction).isEqualTo(testCommand); + assertThat(TestUtils.equals(testArgs, args)).isTrue(); + latch.countDown(); + return new SessionResult(RESULT_SUCCESS).asFuture(); + } + }; + session = + new MediaSession.Builder(context, player) + .setId("sendCommand") + .setSessionCallback(callback) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + controller.sendCommand(testCommand, testArgs, /* cb= */ null); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void controllerCallback_sessionRejects() throws Exception { + SessionCallback sessionCallback = + new SessionCallback() { + @Nullable + @Override + public MediaSession.ConnectResult onConnect( + MediaSession session, ControllerInfo controller) { + return null; + } + }; + session = + new MediaSession.Builder(context, player) + .setId("controllerCallback_sessionRejects") + .setSessionCallback(sessionCallback) + .build(); + + // Session will not accept the controller's commands. + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + controller.getTransportControls().play(); + assertThat(player.countDownLatch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); + } + + @Test + public void prepareFromMediaUri() throws Exception { + Uri mediaUri = Uri.parse("foo://bar"); + Bundle bundle = new Bundle(); + bundle.putString("key", "value"); + CountDownLatch latch = new CountDownLatch(1); + SessionCallback callback = + new TestSessionCallback() { + @Override + public int onSetMediaUri( + @NonNull MediaSession session, + @NonNull ControllerInfo controller, + @NonNull Uri uri, + Bundle extras) { + assertThat(uri).isEqualTo(mediaUri); + assertThat(TestUtils.equals(bundle, extras)).isTrue(); + latch.countDown(); + return RESULT_SUCCESS; + } + }; + session = + new MediaSession.Builder(context, player) + .setId("prepareFromMediaUri") + .setSessionCallback(callback) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + controller.getTransportControls().prepareFromUri(mediaUri, bundle); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.prepareCalled).isTrue(); + } + + @Test + public void playFromMediaUri() throws Exception { + Uri request = Uri.parse("foo://bar"); + Bundle bundle = new Bundle(); + bundle.putString("key", "value"); + CountDownLatch latch = new CountDownLatch(1); + SessionCallback callback = + new TestSessionCallback() { + @Override + public int onSetMediaUri( + @NonNull MediaSession session, + @NonNull ControllerInfo controller, + @NonNull Uri uri, + Bundle extras) { + assertThat(uri).isEqualTo(request); + assertThat(TestUtils.equals(bundle, extras)).isTrue(); + latch.countDown(); + return RESULT_SUCCESS; + } + }; + session = + new MediaSession.Builder(context, player) + .setId("playFromMediaUri") + .setSessionCallback(callback) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + controller.getTransportControls().playFromUri(request, bundle); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.playCalled).isTrue(); + } + + @Test + public void prepareFromMediaId() throws Exception { + String request = "media_id"; + Bundle bundle = new Bundle(); + bundle.putString("key", "value"); + CountDownLatch latch = new CountDownLatch(1); + SessionCallback callback = + new TestSessionCallback() { + @Override + public int onSetMediaUri( + @NonNull MediaSession session, + @NonNull ControllerInfo controller, + @NonNull Uri uri, + Bundle extras) { + assertThat(uri.toString()) + .isEqualTo("androidx://media2-session/prepareFromMediaId?id=" + request); + assertThat(TestUtils.equals(bundle, extras)).isTrue(); + latch.countDown(); + return RESULT_SUCCESS; + } + }; + session = + new MediaSession.Builder(context, player) + .setId("prepareFromMediaId") + .setSessionCallback(callback) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + controller.getTransportControls().prepareFromMediaId(request, bundle); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.prepareCalled).isTrue(); + } + + @Test + public void playFromMediaId() throws Exception { + String mediaId = "media_id"; + Bundle bundle = new Bundle(); + bundle.putString("key", "value"); + CountDownLatch latch = new CountDownLatch(1); + SessionCallback callback = + new TestSessionCallback() { + @Override + public int onSetMediaUri( + @NonNull MediaSession session, + @NonNull ControllerInfo controller, + @NonNull Uri uri, + Bundle extras) { + assertThat(uri.toString()) + .isEqualTo("androidx://media2-session/playFromMediaId?id=" + mediaId); + assertThat(TestUtils.equals(bundle, extras)).isTrue(); + latch.countDown(); + return RESULT_SUCCESS; + } + }; + session = + new MediaSession.Builder(context, player) + .setId("playFromMediaId") + .setSessionCallback(callback) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + controller.getTransportControls().playFromMediaId(mediaId, bundle); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.playCalled).isTrue(); + } + + @Test + public void prepareFromSearch() throws Exception { + String query = "test_query"; + Bundle bundle = new Bundle(); + bundle.putString("key", "value"); + CountDownLatch latch = new CountDownLatch(1); + SessionCallback callback = + new TestSessionCallback() { + @Override + public int onSetMediaUri( + @NonNull MediaSession session, + @NonNull ControllerInfo controller, + @NonNull Uri uri, + Bundle extras) { + assertThat(uri.toString()) + .isEqualTo("androidx://media2-session/prepareFromSearch?query=" + query); + assertThat(TestUtils.equals(bundle, extras)).isTrue(); + latch.countDown(); + return RESULT_SUCCESS; + } + }; + session = + new MediaSession.Builder(context, player) + .setId("prepareFromSearch") + .setSessionCallback(callback) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + controller.getTransportControls().prepareFromSearch(query, bundle); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.prepareCalled).isTrue(); + } + + @Test + public void playFromSearch() throws Exception { + String query = "test_query"; + Bundle bundle = new Bundle(); + bundle.putString("key", "value"); + CountDownLatch latch = new CountDownLatch(1); + SessionCallback callback = + new TestSessionCallback() { + @Override + public int onSetMediaUri( + @NonNull MediaSession session, + @NonNull ControllerInfo controller, + @NonNull Uri uri, + Bundle extras) { + assertThat(uri.toString()) + .isEqualTo("androidx://media2-session/playFromSearch?query=" + query); + assertThat(TestUtils.equals(bundle, extras)).isTrue(); + latch.countDown(); + return RESULT_SUCCESS; + } + }; + session = + new MediaSession.Builder(context, player) + .setId("playFromSearch") + .setSessionCallback(callback) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + controller.getTransportControls().playFromSearch(query, bundle); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.playCalled).isTrue(); + } + + @Test + public void setRating() throws Exception { + int ratingType = RatingCompat.RATING_5_STARS; + float ratingValue = 3.5f; + RatingCompat rating = RatingCompat.newStarRating(ratingType, ratingValue); + String mediaId = "media_id"; + + CountDownLatch latch = new CountDownLatch(1); + SessionCallback callback = + new TestSessionCallback() { + @Override + @NonNull + public ListenableFuture onSetRating( + @NonNull MediaSession session, + @NonNull ControllerInfo controller, + @NonNull String mediaIdOut, + @NonNull Rating ratingOut) { + assertThat(mediaIdOut).isEqualTo(mediaId); + assertThat(ratingOut).isEqualTo(MediaUtils.convertToRating(rating)); + latch.countDown(); + return new SessionResult(RESULT_SUCCESS).asFuture(); + } + }; + + handler.postAndSync( + () -> { + player.currentMediaItem = MediaTestUtils.createConvergedMediaItem(mediaId); + }); + session = + new MediaSession.Builder(context, player) + .setId("setRating") + .setSessionCallback(callback) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + controller.getTransportControls().setRating(rating); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void onCommandRequest() throws Exception { + ArrayList commands = new ArrayList<>(); + CountDownLatch latchForPause = new CountDownLatch(1); + SessionCallback callback = + new TestSessionCallback() { + @Override + public int onPlayerCommandRequest( + @NonNull MediaSession session, + @NonNull ControllerInfo controllerInfo, + @Player.Command int command) { + assertThat(controllerInfo.isTrusted()).isFalse(); + commands.add(command); + if (command == COMMAND_PLAY_PAUSE) { + latchForPause.countDown(); + return RESULT_ERROR_INVALID_STATE; + } + return RESULT_SUCCESS; + } + }; + + session = + new MediaSession.Builder(context, player) + .setId("onPlayerCommandRequest") + .setSessionCallback(callback) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + + controller.getTransportControls().pause(); + assertThat(latchForPause.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.countDownLatch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); + assertThat(player.pauseCalled).isFalse(); + assertThat(commands).hasSize(1); + assertThat(commands.get(0)).isEqualTo(COMMAND_PLAY_PAUSE); + + controller.getTransportControls().prepare(); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.prepareCalled).isTrue(); + assertThat(player.pauseCalled).isFalse(); + assertThat(commands).hasSize(2); + assertThat(commands.get(1)).isEqualTo(COMMAND_PREPARE_STOP); + } + + /** Test potential deadlock for calls between controller and session. */ + @Test + public void deadlock() throws Exception { + MockPlayer player = new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build(); + session = + new MediaSession.Builder(context, player) + .setId("deadlock") + .setSessionCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + // This may hang if deadlock happens. + handler.postAndSync( + () -> { + int state = STATE_IDLE; + for (int i = 0; i < 100; i++) { + // triggers call from session to controller. + player.notifyPlaybackStateChanged(state); + // triggers call from controller to session. + controller.getTransportControls().play(); + + // Repeat above + player.notifyPlaybackStateChanged(state); + controller.getTransportControls().pause(); + player.notifyPlaybackStateChanged(state); + controller.getTransportControls().stop(); + player.notifyPlaybackStateChanged(state); + controller.getTransportControls().skipToNext(); + player.notifyPlaybackStateChanged(state); + controller.getTransportControls().skipToPrevious(); + } + }, + LONG_TIMEOUT_MS); + } + + @Test + public void closedSession_ignoresController() throws Exception { + String sessionId = "closedSession_ignoresController"; + session = + new MediaSession.Builder(context, player) + .setId(sessionId) + .setSessionCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompatToken(), /* waitForConnection= */ true); + session.release(); + session = null; + + controller.getTransportControls().play(); + assertThat(player.countDownLatch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); + + // Ensure that the controller cannot use newly create session with the same ID. + // Recreated session has different session stub, so previously created controller + // shouldn't be available. + session = + new MediaSession.Builder(context, player) + .setId(sessionId) + .setSessionCallback(new TestSessionCallback()) + .build(); + + controller.getTransportControls().play(); + assertThat(player.countDownLatch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); + } + + private static class TestSessionCallback extends SessionCallback { + + @Nullable + @Override + public MediaSession.ConnectResult onConnect(MediaSession session, ControllerInfo controller) { + if (EXPECTED_CONTROLLER_PACKAGE_NAME.equals(controller.getPackageName())) { + return super.onConnect(session, controller); + } + return null; + } + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionCompatCallbackWithMediaControllerTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionCompatCallbackWithMediaControllerTest.java new file mode 100644 index 0000000000..42927146da --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionCompatCallbackWithMediaControllerTest.java @@ -0,0 +1,1169 @@ +/* + * 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.google.android.exoplayer2.session; + +import static android.support.v4.media.session.MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS; +import static com.google.android.exoplayer2.session.MediaConstants.MEDIA_URI_AUTHORITY; +import static com.google.android.exoplayer2.session.MediaConstants.MEDIA_URI_PATH_SET_MEDIA_URI; +import static com.google.android.exoplayer2.session.MediaConstants.MEDIA_URI_QUERY_ID; +import static com.google.android.exoplayer2.session.MediaConstants.MEDIA_URI_QUERY_QUERY; +import static com.google.android.exoplayer2.session.MediaConstants.MEDIA_URI_QUERY_URI; +import static com.google.android.exoplayer2.session.MediaConstants.MEDIA_URI_SCHEME; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.NO_RESPONSE_TIMEOUT_MS; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.VOLUME_CHANGE_TIMEOUT_MS; +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.ResultReceiver; +import android.support.v4.media.MediaDescriptionCompat; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.RatingCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.MediaSessionCompat.QueueItem; +import android.support.v4.media.session.PlaybackStateCompat; +import android.support.v4.media.session.PlaybackStateCompat.RepeatMode; +import android.support.v4.media.session.PlaybackStateCompat.ShuffleMode; +import androidx.media.AudioManagerCompat; +import androidx.media.VolumeProviderCompat; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SdkSuppress; +import androidx.test.filters.SmallTest; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Rating; +import com.google.android.exoplayer2.StarRating; +import com.google.android.exoplayer2.session.vct.common.HandlerThreadTestRule; +import com.google.android.exoplayer2.session.vct.common.MainLooperTestRule; +import com.google.android.exoplayer2.session.vct.common.MockActivity; +import com.google.android.exoplayer2.session.vct.common.PollingCheck; +import com.google.android.exoplayer2.session.vct.common.TestUtils; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link MediaSessionCompat.Callback} with {@link MediaController}. */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class MediaSessionCompatCallbackWithMediaControllerTest { + private static final String TAG = "MediaControllerTest"; + + // The maximum time to wait for an operation. + private static final long TIMEOUT_MS = 3000L; + + @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); + + @Rule public final HandlerThreadTestRule threadTestRule = new HandlerThreadTestRule(TAG); + + @Rule public final RemoteControllerTestRule controllerTestRule = new RemoteControllerTestRule(); + + private Context context; + private MediaSessionCompat session; + private MediaSessionCallback sessionCallback; + private AudioManager audioManager; + + @Before + public void setUp() { + context = ApplicationProvider.getApplicationContext(); + Intent sessionActivity = new Intent(context, MockActivity.class); + // Create this test specific MediaSession to use our own Handler. + PendingIntent intent = PendingIntent.getActivity(context, 0, sessionActivity, 0); + + sessionCallback = new MediaSessionCallback(); + session = new MediaSessionCompat(context, TAG + "Compat"); + session.setCallback(sessionCallback, threadTestRule.getHandler()); + session.setSessionActivity(intent); + session.setActive(true); + + audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + } + + @After + public void cleanUp() { + if (session != null) { + session.release(); + session = null; + } + } + + private RemoteMediaController createControllerAndWaitConnection() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + AtomicReference sessionToken2 = new AtomicReference<>(); + SessionToken.createSessionToken( + context, + session.getSessionToken(), + (compatToken, sessionToken) -> { + assertThat(sessionToken.isLegacySession()).isTrue(); + sessionToken2.set(sessionToken); + latch.countDown(); + }); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + return controllerTestRule.createRemoteController(sessionToken2.get(), true, null); + } + + @Test + public void play() throws Exception { + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.play(); + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onPlayCalledCount).isEqualTo(1); + } + + @Test + public void pause() throws Exception { + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.pause(); + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onPauseCalled).isEqualTo(true); + } + + @Test + public void prepare() throws Exception { + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.prepare(); + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onPrepareCalled).isEqualTo(true); + } + + @Test + public void stop() throws Exception { + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.stop(); + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onStopCalled).isEqualTo(true); + } + + @Test + public void seekToDefaultPosition() throws Exception { + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.seekToDefaultPosition(); + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onSeekToCalled).isTrue(); + assertThat(sessionCallback.seekPosition).isEqualTo(/* pos= */ 0); + } + + @Test + public void seekToDefaultPosition_withWindowIndex() throws Exception { + int testWindowIndex = 1; + List testQueue = MediaTestUtils.createQueueItems(/* size= */ 3); + + session.setQueue(testQueue); + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + RemoteMediaController controller = createControllerAndWaitConnection(); + + sessionCallback.reset(2); + + controller.seekToDefaultPosition(testWindowIndex); + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onSkipToQueueItemCalled).isTrue(); + assertThat(sessionCallback.queueItemId).isEqualTo(testQueue.get(testWindowIndex).getQueueId()); + assertThat(sessionCallback.onSeekToCalled).isTrue(); + assertThat(sessionCallback.seekPosition).isEqualTo(/* pos= */ 0); + } + + @Test + public void seekTo() throws Exception { + long testPositionMs = 12125L; + + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.seekTo(testPositionMs); + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onSeekToCalled).isTrue(); + assertThat(sessionCallback.seekPosition).isEqualTo(testPositionMs); + } + + @Test + public void seekTo_withWindowIndex() throws Exception { + int testWindowIndex = 1; + long testPositionMs = 12L; + + List testQueue = MediaTestUtils.createQueueItems(/* size= */ 3); + + session.setQueue(testQueue); + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + RemoteMediaController controller = createControllerAndWaitConnection(); + + sessionCallback.reset(2); + + controller.seekTo(testWindowIndex, testPositionMs); + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onSkipToQueueItemCalled).isTrue(); + assertThat(sessionCallback.queueItemId).isEqualTo(testQueue.get(testWindowIndex).getQueueId()); + assertThat(sessionCallback.onSeekToCalled).isTrue(); + assertThat(sessionCallback.seekPosition).isEqualTo(testPositionMs); + } + + @Test + public void setPlaybackSpeed_notifiesOnSetPlaybackSpeed() throws Exception { + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + float testSpeed = 2.0f; + controller.setPlaybackSpeed(testSpeed); + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onSetPlaybackSpeedCalled).isTrue(); + assertThat(sessionCallback.speed).isEqualTo(testSpeed); + } + + @Test + public void setPlaybackParameters_notifiesOnSetPlaybackSpeed() throws Exception { + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + PlaybackParameters playbackParameters = new PlaybackParameters(/* speed= */ 1.2f); + controller.setPlaybackParameters(playbackParameters); + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onSetPlaybackSpeedCalled).isTrue(); + assertThat(sessionCallback.speed).isEqualTo(playbackParameters.speed); + } + + @Test + public void setPlaybackParameters_withDefault_notifiesOnSetPlaybackSpeedWithDefault() + throws Exception { + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.setPlaybackParameters(PlaybackParameters.DEFAULT); + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onSetPlaybackSpeedCalled).isTrue(); + assertThat(sessionCallback.speed).isEqualTo(PlaybackParameters.DEFAULT.speed); + } + + @Test + public void addMediaItems() throws Exception { + int size = 2; + List testList = MediaTestUtils.createConvergedMediaItems(size); + List testQueue = MediaUtils.convertToQueueItemList(testList); + + session.setQueue(testQueue); + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(size); + + int testIndex = 1; + controller.addMediaItems(testIndex, testList); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onAddQueueItemAtCalledCount).isEqualTo(size); + for (int i = 0; i < size; i++) { + assertThat(sessionCallback.queueIndices.get(i)).isEqualTo(testIndex + i); + assertThat(sessionCallback.queueDescriptionListForAdd.get(i).getMediaId()) + .isEqualTo(testList.get(i).mediaId); + } + } + + @Test + public void removeMediaItems() throws Exception { + List testList = MediaTestUtils.createConvergedMediaItems(/* size= */ 4); + int fromIndex = 1; + int toIndex = 3; + int count = toIndex - fromIndex; + + session.setQueue(MediaUtils.convertToQueueItemList(testList)); + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(count); + + controller.removeMediaItems(fromIndex, toIndex); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onRemoveQueueItemCalledCount).isEqualTo(count); + for (int i = 0; i < count; i++) { + assertThat(sessionCallback.queueDescriptionListForRemove.get(i).getMediaId()) + .isEqualTo(testList.get(fromIndex + i).mediaId); + } + } + + @Test + public void previous() throws Exception { + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.previous(); + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onSkipToPreviousCalled).isTrue(); + } + + @Test + public void next() throws Exception { + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.next(); + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onSkipToNextCalled).isTrue(); + } + + @Test + public void setShuffleMode() throws Exception { + session.setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_NONE); + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.setShuffleModeEnabled(true); + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onSetShuffleModeCalled).isTrue(); + assertThat(sessionCallback.shuffleMode).isEqualTo(PlaybackStateCompat.SHUFFLE_MODE_ALL); + } + + @Test + public void setRepeatMode() throws Exception { + int testRepeatMode = Player.REPEAT_MODE_ALL; + + session.setRepeatMode(PlaybackStateCompat.REPEAT_MODE_NONE); + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.setRepeatMode(testRepeatMode); + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onSetRepeatModeCalled).isTrue(); + assertThat(sessionCallback.repeatMode).isEqualTo(testRepeatMode); + } + + @Test + public void setDeviceVolume_forRemotePlayback_callsSetVolumeTo() throws Exception { + int maxVolume = 100; + int currentVolume = 23; + int volumeControlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE; + TestVolumeProvider volumeProvider = + new TestVolumeProvider(volumeControlType, maxVolume, currentVolume); + session.setPlaybackToRemote(volumeProvider); + RemoteMediaController controller = createControllerAndWaitConnection(); + + int targetVolume = 50; + controller.setDeviceVolume(targetVolume); + assertThat(volumeProvider.latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(volumeProvider.setVolumeToCalled).isTrue(); + assertThat(volumeProvider.volume).isEqualTo(targetVolume); + } + + @Test + public void increaseDeviceVolume_forRemotePlayback_callsAdjustVolumeWithDirectionRaise() + throws Exception { + int maxVolume = 100; + int currentVolume = 23; + int volumeControlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE; + TestVolumeProvider volumeProvider = + new TestVolumeProvider(volumeControlType, maxVolume, currentVolume); + session.setPlaybackToRemote(volumeProvider); + RemoteMediaController controller = createControllerAndWaitConnection(); + + controller.increaseDeviceVolume(); + assertThat(volumeProvider.latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(volumeProvider.adjustVolumeCalled).isTrue(); + assertThat(volumeProvider.direction).isEqualTo(AudioManager.ADJUST_RAISE); + } + + @Test + public void decreaseDeviceVolume_forRemotePlayback_callsAdjustVolumeWithDirectionLower() + throws Exception { + int maxVolume = 100; + int currentVolume = 23; + int volumeControlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE; + TestVolumeProvider volumeProvider = + new TestVolumeProvider(volumeControlType, maxVolume, currentVolume); + session.setPlaybackToRemote(volumeProvider); + RemoteMediaController controller = createControllerAndWaitConnection(); + + controller.decreaseDeviceVolume(); + assertThat(volumeProvider.latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(volumeProvider.adjustVolumeCalled).isTrue(); + assertThat(volumeProvider.direction).isEqualTo(AudioManager.ADJUST_LOWER); + } + + @Test + public void setDeviceVolume_forLocalPlayback_setsStreamVolume() throws Exception { + if (Build.VERSION.SDK_INT >= 21 && audioManager.isVolumeFixed()) { + // This test is not eligible for this device. + return; + } + + // STREAM_ALARM in order not to consider 'Do Not Disturb' or 'Volume limit'. + int stream = AudioManager.STREAM_ALARM; + int maxVolume = AudioManagerCompat.getStreamMaxVolume(audioManager, stream); + int minVolume = AudioManagerCompat.getStreamMinVolume(audioManager, stream); + if (maxVolume <= minVolume) { + return; + } + session.setPlaybackToLocal(stream); + RemoteMediaController controller = createControllerAndWaitConnection(); + int originalVolume = audioManager.getStreamVolume(stream); + int targetVolume = originalVolume == minVolume ? originalVolume + 1 : originalVolume - 1; + + controller.setDeviceVolume(targetVolume); + PollingCheck.waitFor( + VOLUME_CHANGE_TIMEOUT_MS, () -> targetVolume == audioManager.getStreamVolume(stream)); + + // Set back to original volume. + audioManager.setStreamVolume(stream, originalVolume, /* flags= */ 0); + } + + @Test + public void increaseDeviceVolume_forLocalPlayback_increasesStreamVolume() throws Exception { + if (Build.VERSION.SDK_INT >= 21 && audioManager.isVolumeFixed()) { + // This test is not eligible for this device. + return; + } + + // STREAM_ALARM in order not to consider 'Do Not Disturb' or 'Volume limit'. + int stream = AudioManager.STREAM_ALARM; + int maxVolume = AudioManagerCompat.getStreamMaxVolume(audioManager, stream); + int minVolume = AudioManagerCompat.getStreamMinVolume(audioManager, stream); + if (maxVolume <= minVolume) { + return; + } + session.setPlaybackToLocal(stream); + RemoteMediaController controller = createControllerAndWaitConnection(); + int originalVolume = audioManager.getStreamVolume(stream); + audioManager.setStreamVolume(stream, minVolume, /* flags= */ 0); + int targetVolume = minVolume + 1; + + controller.increaseDeviceVolume(); + PollingCheck.waitFor( + VOLUME_CHANGE_TIMEOUT_MS, () -> targetVolume == audioManager.getStreamVolume(stream)); + + // Set back to original volume. + audioManager.setStreamVolume(stream, originalVolume, /* flags= */ 0); + } + + @Test + public void decreaseDeviceVolume_forLocalPlayback_decreasesStreamVolume() throws Exception { + if (Build.VERSION.SDK_INT >= 21 && audioManager.isVolumeFixed()) { + // This test is not eligible for this device. + return; + } + + // STREAM_ALARM in order not to consider 'Do Not Disturb' or 'Volume limit'. + int stream = AudioManager.STREAM_ALARM; + int maxVolume = AudioManagerCompat.getStreamMaxVolume(audioManager, stream); + int minVolume = AudioManagerCompat.getStreamMinVolume(audioManager, stream); + if (maxVolume <= minVolume) { + return; + } + session.setPlaybackToLocal(stream); + RemoteMediaController controller = createControllerAndWaitConnection(); + int originalVolume = audioManager.getStreamVolume(stream); + audioManager.setStreamVolume(stream, maxVolume, /* flags= */ 0); + int targetVolume = maxVolume - 1; + + controller.decreaseDeviceVolume(); + PollingCheck.waitFor( + VOLUME_CHANGE_TIMEOUT_MS, () -> targetVolume == audioManager.getStreamVolume(stream)); + + // Set back to original volume. + audioManager.setStreamVolume(stream, originalVolume, /* flags= */ 0); + } + + @Test + @SdkSuppress(minSdkVersion = 23) + public void setDeviceMuted_mute_forLocalPlayback_mutesStreamVolume() throws Exception { + if (audioManager.isVolumeFixed()) { + // This test is not eligible for this device. + return; + } + + int stream = AudioManager.STREAM_MUSIC; + int maxVolume = AudioManagerCompat.getStreamMaxVolume(audioManager, stream); + int minVolume = AudioManagerCompat.getStreamMinVolume(audioManager, stream); + if (maxVolume <= minVolume) { + return; + } + session.setPlaybackToLocal(stream); + RemoteMediaController controller = createControllerAndWaitConnection(); + boolean wasMuted = audioManager.isStreamMute(stream); + audioManager.adjustStreamVolume(stream, AudioManager.ADJUST_UNMUTE, /* flags= */ 0); + + controller.setDeviceMuted(true); + PollingCheck.waitFor(VOLUME_CHANGE_TIMEOUT_MS, () -> audioManager.isStreamMute(stream)); + + // Set back to original mute state. + audioManager.adjustStreamVolume( + stream, wasMuted ? AudioManager.ADJUST_MUTE : AudioManager.ADJUST_UNMUTE, /* flags= */ 0); + } + + @Test + @SdkSuppress(minSdkVersion = 23) + public void setDeviceMuted_unmute_forLocalPlayback_unmutesStreamVolume() throws Exception { + if (audioManager.isVolumeFixed()) { + // This test is not eligible for this device. + return; + } + + int stream = AudioManager.STREAM_MUSIC; + int maxVolume = AudioManagerCompat.getStreamMaxVolume(audioManager, stream); + int minVolume = AudioManagerCompat.getStreamMinVolume(audioManager, stream); + if (maxVolume <= minVolume) { + return; + } + session.setPlaybackToLocal(stream); + RemoteMediaController controller = createControllerAndWaitConnection(); + boolean wasMuted = audioManager.isStreamMute(stream); + audioManager.adjustStreamVolume(stream, AudioManager.ADJUST_MUTE, /* flags= */ 0); + + controller.setDeviceMuted(false); + PollingCheck.waitFor(VOLUME_CHANGE_TIMEOUT_MS, () -> !audioManager.isStreamMute(stream)); + + // Set back to original mute state. + audioManager.adjustStreamVolume( + stream, wasMuted ? AudioManager.ADJUST_MUTE : AudioManager.ADJUST_UNMUTE, /* flags= */ 0); + } + + @Test + public void sendCustomCommand() throws Exception { + String command = "test_custom_command"; + Bundle testArgs = new Bundle(); + testArgs.putString("args", "test_args"); + SessionCommand testCommand = new SessionCommand(command, null); + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + SessionResult result = controller.sendCustomCommand(testCommand, testArgs); + assertThat(result.resultCode).isEqualTo(SessionResult.RESULT_SUCCESS); + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onCommandCalled).isTrue(); + assertThat(sessionCallback.command).isEqualTo(command); + assertThat(TestUtils.equals(testArgs, sessionCallback.extras)).isTrue(); + } + + @Test + public void setRating() throws Exception { + float ratingValue = 3.5f; + Rating rating2 = new StarRating(5, ratingValue); + String mediaId = "media_id"; + MediaMetadataCompat metadata = + new MediaMetadataCompat.Builder() + .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId) + .build(); + session.setMetadata(metadata); + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.setRating(mediaId, rating2); + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onSetRatingCalled).isTrue(); + assertThat(MediaUtils.convertToRating(sessionCallback.rating)).isEqualTo(rating2); + } + + @Test + public void setMediaUri_ignored() throws Exception { + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.setMediaUri(Uri.parse("androidx://test?test=xx"), /* extras= */ null); + + assertThat(sessionCallback.await(NO_RESPONSE_TIMEOUT_MS)).isFalse(); + } + + @Test + public void setMediaUri_followedByPrepare_callsPrepareFromMediaId() throws Exception { + String testMediaId = "anyMediaId"; + Bundle testExtras = new Bundle(); + testExtras.putString("testKey", "testValue"); + + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.setMediaUri( + new Uri.Builder() + .scheme(MEDIA_URI_SCHEME) + .authority(MEDIA_URI_AUTHORITY) + .path(MEDIA_URI_PATH_SET_MEDIA_URI) + .appendQueryParameter(MEDIA_URI_QUERY_ID, testMediaId) + .build(), + testExtras); + controller.prepare(); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onPrepareFromMediaIdCalled).isTrue(); + assertThat(sessionCallback.mediaId).isEqualTo(testMediaId); + assertThat(TestUtils.equals(testExtras, sessionCallback.extras)).isTrue(); + assertThat(sessionCallback.query).isNull(); + assertThat(sessionCallback.uri).isNull(); + assertThat(sessionCallback.onPrepareCalled).isFalse(); + } + + @Test + public void setMediaUri_followedByPrepare_callsPrepareFromSearch() throws Exception { + String testSearchQuery = "anyQuery"; + Bundle testExtras = new Bundle(); + testExtras.putString("testKey", "testValue"); + + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.setMediaUri( + new Uri.Builder() + .scheme(MEDIA_URI_SCHEME) + .authority(MEDIA_URI_AUTHORITY) + .path(MEDIA_URI_PATH_SET_MEDIA_URI) + .appendQueryParameter(MEDIA_URI_QUERY_QUERY, testSearchQuery) + .build(), + testExtras); + controller.prepare(); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onPrepareFromSearchCalled).isTrue(); + assertThat(sessionCallback.query).isEqualTo(testSearchQuery); + assertThat(TestUtils.equals(testExtras, sessionCallback.extras)).isTrue(); + assertThat(sessionCallback.mediaId).isNull(); + assertThat(sessionCallback.uri).isNull(); + assertThat(sessionCallback.onPrepareCalled).isFalse(); + } + + @Test + public void setMediaUri_followedByPrepare_callsPrepareFromUri() throws Exception { + Uri testMediaUri = Uri.parse("androidx://jetpack/test?query=android%20media"); + Bundle testExtras = new Bundle(); + testExtras.putString("testKey", "testValue"); + + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.setMediaUri( + new Uri.Builder() + .scheme(MEDIA_URI_SCHEME) + .authority(MEDIA_URI_AUTHORITY) + .path(MEDIA_URI_PATH_SET_MEDIA_URI) + .appendQueryParameter(MEDIA_URI_QUERY_URI, testMediaUri.toString()) + .build(), + testExtras); + controller.prepare(); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onPrepareFromUriCalled).isTrue(); + assertThat(sessionCallback.uri).isEqualTo(testMediaUri); + assertThat(sessionCallback.mediaId).isNull(); + assertThat(sessionCallback.query).isNull(); + assertThat(sessionCallback.onPrepareCalled).isFalse(); + } + + @Test + public void setMediaUri_withoutFormattingFollowedByPrepare_callsPrepareFromUri() + throws Exception { + Uri testMediaUri = Uri.parse("androidx://jetpack/test?query=android%20media"); + Bundle testExtras = new Bundle(); + testExtras.putString("testKey", "testValue"); + + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.setMediaUri(testMediaUri, testExtras); + controller.prepare(); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onPrepareFromUriCalled).isTrue(); + assertThat(sessionCallback.uri).isEqualTo(testMediaUri); + assertThat(sessionCallback.mediaId).isNull(); + assertThat(sessionCallback.query).isNull(); + assertThat(sessionCallback.onPrepareCalled).isFalse(); + } + + @Test + public void setMediaUri_followedByPlay_callsPlayFromMediaId() throws Exception { + String testMediaId = "anyMediaId"; + Bundle testExtras = new Bundle(); + testExtras.putString("testKey", "testValue"); + + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.setMediaUri( + new Uri.Builder() + .scheme(MEDIA_URI_SCHEME) + .authority(MEDIA_URI_AUTHORITY) + .path(MEDIA_URI_PATH_SET_MEDIA_URI) + .appendQueryParameter(MEDIA_URI_QUERY_ID, testMediaId) + .build(), + testExtras); + controller.play(); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onPlayFromMediaIdCalled).isTrue(); + assertThat(sessionCallback.mediaId).isEqualTo(testMediaId); + assertThat(TestUtils.equals(testExtras, sessionCallback.extras)).isTrue(); + assertThat(sessionCallback.query).isNull(); + assertThat(sessionCallback.uri).isNull(); + assertThat(sessionCallback.onPlayCalledCount).isEqualTo(0); + } + + @Test + public void setMediaUri_followedByPlay_callsPlayFromSearch() throws Exception { + String testSearchQuery = "anyQuery"; + Bundle testExtras = new Bundle(); + testExtras.putString("testKey", "testValue"); + + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.setMediaUri( + new Uri.Builder() + .scheme(MEDIA_URI_SCHEME) + .authority(MEDIA_URI_AUTHORITY) + .path(MEDIA_URI_PATH_SET_MEDIA_URI) + .appendQueryParameter(MEDIA_URI_QUERY_QUERY, testSearchQuery) + .build(), + testExtras); + controller.play(); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onPlayFromSearchCalled).isTrue(); + assertThat(sessionCallback.query).isEqualTo(testSearchQuery); + assertThat(TestUtils.equals(testExtras, sessionCallback.extras)).isTrue(); + assertThat(sessionCallback.mediaId).isNull(); + assertThat(sessionCallback.uri).isNull(); + assertThat(sessionCallback.onPlayCalledCount).isEqualTo(0); + } + + @Test + public void setMediaUri_followedByPlay_callsPlayFromUri() throws Exception { + Uri testMediaUri = Uri.parse("androidx://jetpack/test?query=android%20media"); + Bundle testExtras = new Bundle(); + testExtras.putString("testKey", "testValue"); + + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.setMediaUri( + new Uri.Builder() + .scheme(MEDIA_URI_SCHEME) + .authority(MEDIA_URI_AUTHORITY) + .path(MEDIA_URI_PATH_SET_MEDIA_URI) + .appendQueryParameter(MEDIA_URI_QUERY_URI, testMediaUri.toString()) + .build(), + testExtras); + controller.play(); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onPlayFromUriCalled).isTrue(); + assertThat(sessionCallback.uri).isEqualTo(testMediaUri); + assertThat(sessionCallback.mediaId).isNull(); + assertThat(sessionCallback.query).isNull(); + assertThat(sessionCallback.onPlayCalledCount).isEqualTo(0); + } + + @Test + public void setMediaUri_withoutFormattingFollowedByPlay_callsPlayFromUri() throws Exception { + Uri testMediaUri = Uri.parse("androidx://jetpack/test?query=android%20media"); + Bundle testExtras = new Bundle(); + testExtras.putString("testKey", "testValue"); + + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.setMediaUri(testMediaUri, testExtras); + controller.play(); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onPlayFromUriCalled).isTrue(); + assertThat(sessionCallback.uri).isEqualTo(testMediaUri); + assertThat(sessionCallback.mediaId).isNull(); + assertThat(sessionCallback.query).isNull(); + assertThat(sessionCallback.onPlayCalledCount).isEqualTo(0); + } + + @Test + public void setMediaUri_followedByPrepareTwice_callsPrepareFromUriAndPrepare() throws Exception { + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(2); + + controller.setMediaUri(Uri.parse("androidx://test"), null); + + controller.prepare(); + controller.prepare(); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onPrepareFromUriCalled).isTrue(); + assertThat(sessionCallback.onPrepareCalled).isTrue(); + } + + @Test + public void setMediaUri_followedByPlayTwice_callsPlayFromUriAndPlay() throws Exception { + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(2); + + controller.setMediaUri(Uri.parse("androidx://test"), /* extras= */ null); + + controller.play(); + controller.play(); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onPlayFromUriCalled).isTrue(); + assertThat(sessionCallback.onPlayCalledCount).isEqualTo(1); + } + + @Test + public void setMediaUri_multipleCalls_skipped() throws Exception { + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(2); + + Uri testUri1 = Uri.parse("androidx://test1"); + Uri testUri2 = Uri.parse("androidx://test2"); + controller.setMediaUri(testUri1, /* extras= */ null); + controller.setMediaUri(testUri2, /* extras= */ null); + controller.prepare(); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isFalse(); + assertThat(sessionCallback.onPrepareFromUriCalled).isTrue(); + assertThat(sessionCallback.uri).isEqualTo(testUri2); + } + + private void setPlaybackState(int state) { + long allActions = + PlaybackStateCompat.ACTION_PLAY + | PlaybackStateCompat.ACTION_PAUSE + | PlaybackStateCompat.ACTION_PLAY_PAUSE + | PlaybackStateCompat.ACTION_STOP + | PlaybackStateCompat.ACTION_SKIP_TO_NEXT + | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS + | PlaybackStateCompat.ACTION_FAST_FORWARD + | PlaybackStateCompat.ACTION_REWIND; + PlaybackStateCompat playbackState = + new PlaybackStateCompat.Builder().setActions(allActions).setState(state, 0L, 0.0f).build(); + session.setPlaybackState(playbackState); + } + + class TestVolumeProvider extends VolumeProviderCompat { + CountDownLatch latch = new CountDownLatch(1); + boolean setVolumeToCalled; + boolean adjustVolumeCalled; + int volume; + int direction; + + TestVolumeProvider(int controlType, int maxVolume, int currentVolume) { + super(controlType, maxVolume, currentVolume); + } + + @Override + public void onSetVolumeTo(int volume) { + setVolumeToCalled = true; + this.volume = volume; + latch.countDown(); + } + + @Override + public void onAdjustVolume(int direction) { + adjustVolumeCalled = true; + this.direction = direction; + latch.countDown(); + } + } + + private class MediaSessionCallback extends MediaSessionCompat.Callback { + public CountDownLatch latch = new CountDownLatch(1); + public long seekPosition; + public float speed; + public long queueItemId; + public RatingCompat rating; + public String mediaId; + public String query; + public Uri uri; + public String action; + public String command; + public Bundle extras; + public ResultReceiver commandCallback; + public boolean captioningEnabled; + @RepeatMode public int repeatMode; + @ShuffleMode public int shuffleMode; + public final List queueIndices = new ArrayList<>(); + public final List queueDescriptionListForAdd = new ArrayList<>(); + public final List queueDescriptionListForRemove = new ArrayList<>(); + + public int onPlayCalledCount; + public boolean onPauseCalled; + public boolean onStopCalled; + public boolean onSkipToPreviousCalled; + public boolean onSkipToNextCalled; + public boolean onSeekToCalled; + public boolean onSetPlaybackSpeedCalled; + public boolean onSkipToQueueItemCalled; + public boolean onSetRatingCalled; + public boolean onPlayFromMediaIdCalled; + public boolean onPlayFromSearchCalled; + public boolean onPlayFromUriCalled; + public boolean onCustomActionCalled; + public boolean onCommandCalled; + public boolean onPrepareCalled; + public boolean onPrepareFromMediaIdCalled; + public boolean onPrepareFromSearchCalled; + public boolean onPrepareFromUriCalled; + public boolean onSetCaptioningEnabledCalled; + public boolean onSetRepeatModeCalled; + public boolean onSetShuffleModeCalled; + public boolean onAddQueueItemCalled; + public int onAddQueueItemAtCalledCount; + public int onRemoveQueueItemCalledCount; + + public void reset(int count) { + latch = new CountDownLatch(count); + seekPosition = -1; + speed = -1.0f; + queueItemId = -1; + rating = null; + mediaId = null; + query = null; + uri = null; + action = null; + extras = null; + command = null; + commandCallback = null; + captioningEnabled = false; + repeatMode = PlaybackStateCompat.REPEAT_MODE_NONE; + shuffleMode = PlaybackStateCompat.SHUFFLE_MODE_NONE; + queueIndices.clear(); + queueDescriptionListForAdd.clear(); + queueDescriptionListForRemove.clear(); + + onPlayCalledCount = 0; + onPauseCalled = false; + onStopCalled = false; + onSkipToPreviousCalled = false; + onSkipToNextCalled = false; + onSkipToQueueItemCalled = false; + onSeekToCalled = false; + onSetPlaybackSpeedCalled = false; + onSetRatingCalled = false; + onPlayFromMediaIdCalled = false; + onPlayFromSearchCalled = false; + onPlayFromUriCalled = false; + onCustomActionCalled = false; + onCommandCalled = false; + onPrepareCalled = false; + onPrepareFromMediaIdCalled = false; + onPrepareFromSearchCalled = false; + onPrepareFromUriCalled = false; + onSetCaptioningEnabledCalled = false; + onSetRepeatModeCalled = false; + onSetShuffleModeCalled = false; + onAddQueueItemCalled = false; + onAddQueueItemAtCalledCount = 0; + onRemoveQueueItemCalledCount = 0; + } + + public boolean await(long timeoutMs) { + try { + return latch.await(timeoutMs, MILLISECONDS); + } catch (InterruptedException e) { + return false; + } + } + + @Override + public void onPlay() { + onPlayCalledCount++; + setPlaybackState(PlaybackStateCompat.STATE_PLAYING); + latch.countDown(); + } + + @Override + public void onPause() { + onPauseCalled = true; + setPlaybackState(PlaybackStateCompat.STATE_PAUSED); + latch.countDown(); + } + + @Override + public void onStop() { + onStopCalled = true; + setPlaybackState(PlaybackStateCompat.STATE_STOPPED); + latch.countDown(); + } + + @Override + public void onSkipToPrevious() { + onSkipToPreviousCalled = true; + latch.countDown(); + } + + @Override + public void onSkipToNext() { + onSkipToNextCalled = true; + latch.countDown(); + } + + @Override + public void onSeekTo(long pos) { + onSeekToCalled = true; + seekPosition = pos; + latch.countDown(); + } + + @Override + public void onSetPlaybackSpeed(float speed) { + onSetPlaybackSpeedCalled = true; + this.speed = speed; + latch.countDown(); + } + + @Override + public void onSetRating(RatingCompat rating) { + onSetRatingCalled = true; + this.rating = rating; + latch.countDown(); + } + + @Override + public void onPlayFromMediaId(String mediaId, Bundle extras) { + onPlayFromMediaIdCalled = true; + this.mediaId = mediaId; + this.extras = extras; + latch.countDown(); + } + + @Override + public void onPlayFromSearch(String query, Bundle extras) { + onPlayFromSearchCalled = true; + this.query = query; + this.extras = extras; + latch.countDown(); + } + + @Override + public void onPlayFromUri(Uri uri, Bundle extras) { + onPlayFromUriCalled = true; + this.uri = uri; + this.extras = extras; + latch.countDown(); + } + + @Override + public void onCustomAction(String action, Bundle extras) { + onCustomActionCalled = true; + this.action = action; + this.extras = extras; + latch.countDown(); + } + + @Override + public void onSkipToQueueItem(long id) { + onSkipToQueueItemCalled = true; + queueItemId = id; + latch.countDown(); + } + + @Override + public void onCommand(String command, Bundle extras, ResultReceiver cb) { + onCommandCalled = true; + this.command = command; + this.extras = extras; + commandCallback = cb; + cb.send(SessionResult.RESULT_SUCCESS, /* resultData= */ null); + latch.countDown(); + } + + @Override + public void onPrepare() { + onPrepareCalled = true; + latch.countDown(); + } + + @Override + public void onPrepareFromMediaId(String mediaId, Bundle extras) { + onPrepareFromMediaIdCalled = true; + this.mediaId = mediaId; + this.extras = extras; + latch.countDown(); + } + + @Override + public void onPrepareFromSearch(String query, Bundle extras) { + onPrepareFromSearchCalled = true; + this.query = query; + this.extras = extras; + latch.countDown(); + } + + @Override + public void onPrepareFromUri(Uri uri, Bundle extras) { + onPrepareFromUriCalled = true; + this.uri = uri; + this.extras = extras; + latch.countDown(); + } + + @Override + public void onSetRepeatMode(@RepeatMode int repeatMode) { + onSetRepeatModeCalled = true; + this.repeatMode = repeatMode; + session.setRepeatMode(repeatMode); + latch.countDown(); + } + + @Override + public void onAddQueueItem(MediaDescriptionCompat description) { + onAddQueueItemCalled = true; + queueDescriptionListForAdd.add(description); + latch.countDown(); + } + + @Override + public void onAddQueueItem(MediaDescriptionCompat description, int index) { + onAddQueueItemAtCalledCount++; + queueIndices.add(index); + queueDescriptionListForAdd.add(description); + latch.countDown(); + } + + @Override + public void onRemoveQueueItem(MediaDescriptionCompat description) { + onRemoveQueueItemCalledCount++; + queueDescriptionListForRemove.add(description); + latch.countDown(); + } + + @Override + public void onSetCaptioningEnabled(boolean enabled) { + onSetCaptioningEnabledCalled = true; + captioningEnabled = enabled; + latch.countDown(); + } + + @Override + public void onSetShuffleMode(@ShuffleMode int shuffleMode) { + onSetShuffleModeCalled = true; + this.shuffleMode = shuffleMode; + session.setShuffleMode(shuffleMode); + latch.countDown(); + } + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionKeyEventTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionKeyEventTest.java new file mode 100644 index 0000000000..f06cdb86d4 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionKeyEventTest.java @@ -0,0 +1,215 @@ +/* + * 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.google.android.exoplayer2.session; + +import static androidx.media.MediaSessionManager.RemoteUserInfo.LEGACY_CONTROLLER; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.LONG_TIMEOUT_MS; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.TIMEOUT_MS; +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.Context; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.os.Build; +import android.view.KeyEvent; +import androidx.annotation.Nullable; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import androidx.test.filters.SdkSuppress; +import com.google.android.exoplayer2.session.MediaSession.ControllerInfo; +import com.google.android.exoplayer2.session.vct.common.HandlerThreadTestRule; +import com.google.android.exoplayer2.session.vct.common.MainLooperTestRule; +import com.google.android.exoplayer2.session.vct.common.R; +import com.google.android.exoplayer2.session.vct.common.TestHandler; +import java.util.concurrent.CountDownLatch; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for key event handling of {@link MediaSession}. In order to get the media key events, the + * player state is set to 'Playing' before every test method. + */ +@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) // For AudioManager#dispatchMediaKeyEvent() +@RunWith(AndroidJUnit4.class) +@LargeTest +public class MediaSessionKeyEventTest { + + private static String expectedControllerPackageName; + + static { + if (Build.VERSION.SDK_INT >= 28 || Build.VERSION.SDK_INT < 21) { + expectedControllerPackageName = SUPPORT_APP_PACKAGE_NAME; + } else if (Build.VERSION.SDK_INT >= 24) { + // KeyEvent from system service has the package name "android". + expectedControllerPackageName = "android"; + } else { + // In API 21+, MediaSessionCompat#getCurrentControllerInfo always returns fake info. + expectedControllerPackageName = LEGACY_CONTROLLER; + } + } + + @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); + + @Rule + public final HandlerThreadTestRule threadTestRule = + new HandlerThreadTestRule("MediaSessionKeyEventTest"); + + // Intentionally member variable to prevent GC while playback is running. + // Should be only used on the sHandler. + private MediaPlayer mediaPlayer; + + private AudioManager audioManager; + private TestHandler handler; + private MediaSession session; + private MockPlayer player; + private TestSessionCallback sessionCallback; + + @Before + public void setUp() throws Exception { + Context context = ApplicationProvider.getApplicationContext(); + audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + handler = threadTestRule.getHandler(); + player = + new MockPlayer.Builder().setLatchCount(1).setApplicationLooper(handler.getLooper()).build(); + + sessionCallback = new TestSessionCallback(); + session = new MediaSession.Builder(context, player).setSessionCallback(sessionCallback).build(); + + // Here's the requirement for an app to receive media key events via MediaSession. + // - SDK < 26: Player should be playing for receiving key events + // - SDK >= 26: Play a media item in the same process of the session for receiving key events. + handler.postAndSync(() -> player.notifyIsPlayingChanged(/* isPlaying= */ true)); + if (Build.VERSION.SDK_INT >= 26) { + CountDownLatch latch = new CountDownLatch(1); + handler.postAndSync( + () -> { + // Pick the shortest media to finish within the timeout. + mediaPlayer = MediaPlayer.create(context, R.raw.camera_click); + mediaPlayer.setOnCompletionListener( + player -> { + if (mediaPlayer != null) { + mediaPlayer.release(); + mediaPlayer = null; + latch.countDown(); + } + }); + mediaPlayer.start(); + }); + assertThat(latch.await(LONG_TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + } + + @After + public void cleanUp() throws Exception { + handler.postAndSync( + () -> { + if (mediaPlayer != null) { + mediaPlayer.release(); + mediaPlayer = null; + } + }); + session.release(); + } + + private void dispatchMediaKeyEvent(int keyCode, boolean doubleTap) { + audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); + audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode)); + if (doubleTap) { + audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); + audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode)); + } + } + + @Test + public void playKeyEvent() throws Exception { + dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY, false); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.playCalled).isTrue(); + } + + @Test + public void pauseKeyEvent() throws Exception { + dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PAUSE, false); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.pauseCalled).isTrue(); + } + + @Test + public void nextKeyEvent() throws Exception { + dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_NEXT, false); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.nextCalled).isTrue(); + } + + @Test + public void previousKeyEvent() throws Exception { + dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PREVIOUS, false); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.previousCalled).isTrue(); + } + + @Test + public void stopKeyEvent() throws Exception { + dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_STOP, false); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.stopCalled).isTrue(); + } + + @Test + public void playPauseKeyEvent_play() throws Exception { + dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, false); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.playCalled).isTrue(); + } + + @Test + public void playPauseKeyEvent_pause() throws Exception { + handler.postAndSync( + () -> { + player.playWhenReady = true; + }); + dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, false); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.pauseCalled).isTrue(); + } + + @Test + public void playPauseKeyEvent_doubleTapIsTranslatedToSkipToNext() throws Exception { + dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, true); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.nextCalled).isTrue(); + assertThat(player.playCalled).isFalse(); + assertThat(player.pauseCalled).isFalse(); + } + + private static class TestSessionCallback extends MediaSession.SessionCallback { + @Nullable + @Override + public MediaSession.ConnectResult onConnect(MediaSession session, ControllerInfo controller) { + if (expectedControllerPackageName.equals(controller.getPackageName())) { + return super.onConnect(session, controller); + } + return null; + } + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionManagerTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionManagerTest.java new file mode 100644 index 0000000000..9e9b628c98 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionManagerTest.java @@ -0,0 +1,75 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.MOCK_MEDIA2_LIBRARY_SERVICE; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.MOCK_MEDIA2_SESSION_SERVICE; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; +import static com.google.common.truth.Truth.assertThat; + +import android.content.ComponentName; +import android.content.Context; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import com.google.android.exoplayer2.session.vct.common.MainLooperTestRule; +import java.util.Set; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link MediaSessionManager}. */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class MediaSessionManagerTest { + + private static final ComponentName MOCK_BROWSER_SERVICE_COMPAT_NAME = + new ComponentName( + SUPPORT_APP_PACKAGE_NAME, MockMediaBrowserServiceCompat.class.getCanonicalName()); + + @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); + + private Context context; + + @Before + public void setUp() throws Exception { + context = ApplicationProvider.getApplicationContext(); + } + + @Test + public void getSessionServiceTokens() { + boolean hasMockBrowserServiceCompat = false; + boolean hasMockSessionService2 = false; + boolean hasMockLibraryService2 = false; + MediaSessionManager sessionManager = MediaSessionManager.getInstance(context); + Set serviceTokens = sessionManager.getSessionServiceTokens(); + for (SessionToken token : serviceTokens) { + ComponentName componentName = token.getComponentName(); + if (MOCK_BROWSER_SERVICE_COMPAT_NAME.equals(componentName)) { + hasMockBrowserServiceCompat = true; + } else if (MOCK_MEDIA2_SESSION_SERVICE.equals(componentName)) { + hasMockSessionService2 = true; + } else if (MOCK_MEDIA2_LIBRARY_SERVICE.equals(componentName)) { + hasMockLibraryService2 = true; + } + } + assertThat(hasMockBrowserServiceCompat).isTrue(); + assertThat(hasMockSessionService2).isTrue(); + assertThat(hasMockLibraryService2).isTrue(); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionPermissionTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionPermissionTest.java new file mode 100644 index 0000000000..de9a389391 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionPermissionTest.java @@ -0,0 +1,409 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.Player.COMMAND_ADJUST_DEVICE_VOLUME; +import static com.google.android.exoplayer2.Player.COMMAND_CHANGE_MEDIA_ITEMS; +import static com.google.android.exoplayer2.Player.COMMAND_PLAY_PAUSE; +import static com.google.android.exoplayer2.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM; +import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM; +import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM; +import static com.google.android.exoplayer2.Player.COMMAND_SET_DEVICE_VOLUME; +import static com.google.android.exoplayer2.Player.COMMAND_SET_MEDIA_ITEMS_METADATA; +import static com.google.android.exoplayer2.session.MediaUtils.createPlayerCommandsWith; +import static com.google.android.exoplayer2.session.MediaUtils.createPlayerCommandsWithout; +import static com.google.android.exoplayer2.session.SessionCommand.COMMAND_CODE_SESSION_SET_MEDIA_URI; +import static com.google.android.exoplayer2.session.SessionCommand.COMMAND_CODE_SESSION_SET_RATING; +import static com.google.android.exoplayer2.session.SessionResult.RESULT_SUCCESS; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.NO_RESPONSE_TIMEOUT_MS; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.TIMEOUT_MS; +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.text.TextUtils; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import com.google.android.exoplayer2.MediaMetadata; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Rating; +import com.google.android.exoplayer2.StarRating; +import com.google.android.exoplayer2.session.MediaSession.ControllerInfo; +import com.google.android.exoplayer2.session.vct.common.HandlerThreadTestRule; +import com.google.android.exoplayer2.session.vct.common.MainLooperTestRule; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for permission handling of {@link MediaSession}. */ +@RunWith(AndroidJUnit4.class) +@LargeTest +public class MediaSessionPermissionTest { + private static final String SESSION_ID = "MediaSessionTest_permission"; + + @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); + + @Rule + public final HandlerThreadTestRule threadTestRule = + new HandlerThreadTestRule("MediaSessionPermissionTest"); + + @Rule public final RemoteControllerTestRule controllerTestRule = new RemoteControllerTestRule(); + + private Context context; + private MockPlayer player; + private MediaSession session; + private MySessionCallback callback; + + @Before + public void setUp() { + context = ApplicationProvider.getApplicationContext(); + } + + @After + public void cleanUp() { + if (session != null) { + session.release(); + session = null; + } + player = null; + callback = null; + } + + private void createSessionWithAvailableCommands( + SessionCommands sessionCommands, Player.Commands playerCommands) { + player = + new MockPlayer.Builder() + .setLatchCount(1) + .setApplicationLooper(threadTestRule.getHandler().getLooper()) + .build(); + callback = + new MySessionCallback() { + @Nullable + @Override + public MediaSession.ConnectResult onConnect( + MediaSession session, ControllerInfo controller) { + if (!TextUtils.equals(SUPPORT_APP_PACKAGE_NAME, controller.getPackageName())) { + return null; + } + return new MediaSession.ConnectResult(sessionCommands, playerCommands); + } + }; + if (this.session != null) { + this.session.release(); + } + this.session = + new MediaSession.Builder(context, player) + .setId(SESSION_ID) + .setSessionCallback(callback) + .build(); + } + + private SessionCommands createSessionCommandsWith(SessionCommand command) { + return new SessionCommands.Builder().add(command).build(); + } + + private void testOnCommandRequest(int commandCode, PermissionTestTask runnable) throws Exception { + createSessionWithAvailableCommands( + SessionCommands.EMPTY, createPlayerCommandsWith(commandCode)); + runnable.run(controllerTestRule.createRemoteController(session.getToken())); + + assertThat(callback.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(callback.onCommandRequestCalled).isTrue(); + assertThat(callback.command).isEqualTo(commandCode); + + createSessionWithAvailableCommands( + SessionCommands.EMPTY, createPlayerCommandsWithout(commandCode)); + runnable.run(controllerTestRule.createRemoteController(session.getToken())); + + assertThat(callback.countDownLatch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); + assertThat(callback.onCommandRequestCalled).isFalse(); + } + + @Test + public void play() throws Exception { + testOnCommandRequest(COMMAND_PLAY_PAUSE, RemoteMediaController::play); + } + + @Test + public void pause() throws Exception { + testOnCommandRequest(COMMAND_PLAY_PAUSE, RemoteMediaController::pause); + } + + @Test + public void seekTo() throws Exception { + long position = 10; + testOnCommandRequest( + COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, controller -> controller.seekTo(position)); + } + + @Test + public void seekToNextMediaItem() throws Exception { + testOnCommandRequest(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, RemoteMediaController::next); + } + + @Test + public void seekToPreviousMediaItem() throws Exception { + testOnCommandRequest(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, RemoteMediaController::previous); + } + + @Test + public void setPlaylistMetadata() throws Exception { + testOnCommandRequest( + COMMAND_SET_MEDIA_ITEMS_METADATA, + controller -> controller.setPlaylistMetadata(MediaMetadata.EMPTY)); + } + + @Test + public void setMediaItems() throws Exception { + testOnCommandRequest( + COMMAND_CHANGE_MEDIA_ITEMS, + controller -> controller.setMediaItems(Collections.emptyList())); + } + + @Test + public void addMediaItems() throws Exception { + testOnCommandRequest( + COMMAND_CHANGE_MEDIA_ITEMS, + controller -> controller.addMediaItems(/* index= */ 0, Collections.emptyList())); + } + + @Test + public void removeMediaItems() throws Exception { + testOnCommandRequest( + COMMAND_CHANGE_MEDIA_ITEMS, + controller -> controller.removeMediaItems(/* fromIndex= */ 0, /* toIndex= */ 1)); + } + + @Test + public void setDeviceVolume() throws Exception { + testOnCommandRequest(COMMAND_SET_DEVICE_VOLUME, controller -> controller.setDeviceVolume(0)); + } + + @Test + public void increaseDeviceVolume() throws Exception { + testOnCommandRequest(COMMAND_ADJUST_DEVICE_VOLUME, RemoteMediaController::increaseDeviceVolume); + } + + @Test + public void decreaseDeviceVolume() throws Exception { + testOnCommandRequest(COMMAND_ADJUST_DEVICE_VOLUME, RemoteMediaController::decreaseDeviceVolume); + } + + @Test + public void setDeviceMuted() throws Exception { + testOnCommandRequest(COMMAND_SET_DEVICE_VOLUME, controller -> controller.setDeviceMuted(true)); + } + + @Test + public void setMediaUri() throws Exception { + Uri uri = Uri.parse("media://uri"); + createSessionWithAvailableCommands( + createSessionCommandsWith(new SessionCommand(COMMAND_CODE_SESSION_SET_MEDIA_URI)), + Player.Commands.EMPTY); + controllerTestRule + .createRemoteController(session.getToken()) + .setMediaUri(uri, /* extras= */ null); + + assertThat(callback.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(callback.onSetMediaUriCalled).isTrue(); + assertThat(callback.uri).isEqualTo(uri); + assertThat(callback.extras).isNull(); + + createSessionWithAvailableCommands( + createSessionCommandsWith(new SessionCommand(COMMAND_CODE_SESSION_SET_RATING)), + Player.Commands.EMPTY); + controllerTestRule + .createRemoteController(session.getToken()) + .setMediaUri(uri, /* extras= */ null); + assertThat(callback.countDownLatch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); + assertThat(callback.onSetMediaUriCalled).isFalse(); + } + + @Test + public void setRating() throws Exception { + String mediaId = "testSetRating"; + Rating rating = new StarRating(5, 3.5f); + createSessionWithAvailableCommands( + createSessionCommandsWith(new SessionCommand(COMMAND_CODE_SESSION_SET_RATING)), + Player.Commands.EMPTY); + controllerTestRule.createRemoteController(session.getToken()).setRating(mediaId, rating); + + assertThat(callback.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(callback.onSetRatingCalled).isTrue(); + assertThat(callback.mediaId).isEqualTo(mediaId); + assertThat(callback.rating).isEqualTo(rating); + + createSessionWithAvailableCommands( + createSessionCommandsWith(new SessionCommand(COMMAND_CODE_SESSION_SET_MEDIA_URI)), + Player.Commands.EMPTY); + controllerTestRule.createRemoteController(session.getToken()).setRating(mediaId, rating); + assertThat(callback.countDownLatch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); + assertThat(callback.onSetRatingCalled).isFalse(); + } + + @Test + public void changingPermissionForSessionCommandWithSetAvailableCommands() throws Exception { + String mediaId = "testSetRating"; + Rating rating = new StarRating(5, 3.5f); + createSessionWithAvailableCommands( + createSessionCommandsWith(new SessionCommand(COMMAND_CODE_SESSION_SET_RATING)), + Player.Commands.EMPTY); + RemoteMediaController controller = + controllerTestRule.createRemoteController(session.getToken()); + + controller.setRating(mediaId, rating); + assertThat(callback.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(callback.onSetRatingCalled).isTrue(); + callback.reset(); + + // Change allowed commands. + session.setAvailableCommands( + getTestControllerInfo(), + createSessionCommandsWith(new SessionCommand(COMMAND_CODE_SESSION_SET_MEDIA_URI)), + Player.Commands.EMPTY); + + controller.setRating(mediaId, rating); + assertThat(callback.countDownLatch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); + } + + @Test + public void changingPermissionForPlayerCommandWithSetAvailableCommands() throws Exception { + int playPauseCommand = COMMAND_PLAY_PAUSE; + Player.Commands commandsWithPlayPause = createPlayerCommandsWith(playPauseCommand); + Player.Commands commandsWithoutPlayPause = createPlayerCommandsWithout(playPauseCommand); + + // Create session with play/pause command. + createSessionWithAvailableCommands(SessionCommands.EMPTY, commandsWithPlayPause); + // Create player with play/pause command. + player.commands = commandsWithPlayPause; + player.notifyAvailableCommandsChanged(commandsWithPlayPause); + RemoteMediaController controller = + controllerTestRule.createRemoteController(session.getToken()); + + controller.play(); + assertThat(callback.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(callback.onCommandRequestCalled).isTrue(); + assertThat(callback.command).isEqualTo(playPauseCommand); + callback.reset(); + + // Change session to not have play/pause command. + session.setAvailableCommands( + getTestControllerInfo(), SessionCommands.EMPTY, commandsWithoutPlayPause); + + controller.play(); + assertThat(callback.countDownLatch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); + assertThat(callback.onCommandRequestCalled).isFalse(); + } + + private ControllerInfo getTestControllerInfo() { + List controllers = session.getConnectedControllers(); + assertThat(controllers).isNotNull(); + for (int i = 0; i < controllers.size(); i++) { + if (TextUtils.equals(SUPPORT_APP_PACKAGE_NAME, controllers.get(i).getPackageName())) { + return controllers.get(i); + } + } + throw new IllegalStateException("Failed to get test controller info"); + } + + /* @FunctionalInterface */ + private interface PermissionTestTask { + void run(@NonNull RemoteMediaController controller) throws Exception; + } + + private static class MySessionCallback extends MediaSession.SessionCallback { + public CountDownLatch countDownLatch; + + public @Player.Command int command; + public String mediaId; + public Uri uri; + public Bundle extras; + public Rating rating; + + public boolean onCommandRequestCalled; + public boolean onSetMediaUriCalled; + public boolean onSetRatingCalled; + + public MySessionCallback() { + countDownLatch = new CountDownLatch(1); + } + + public void reset() { + countDownLatch = new CountDownLatch(1); + + mediaId = null; + + onCommandRequestCalled = false; + onSetMediaUriCalled = false; + onSetRatingCalled = false; + } + + @Override + public int onPlayerCommandRequest( + @NonNull MediaSession session, + @NonNull ControllerInfo controller, + @Player.Command int command) { + assertThat(TextUtils.equals(SUPPORT_APP_PACKAGE_NAME, controller.getPackageName())).isTrue(); + onCommandRequestCalled = true; + this.command = command; + countDownLatch.countDown(); + return super.onPlayerCommandRequest(session, controller, command); + } + + @Override + public int onSetMediaUri( + @NonNull MediaSession session, + @NonNull ControllerInfo controller, + @NonNull Uri uri, + @Nullable Bundle extras) { + assertThat(TextUtils.equals(SUPPORT_APP_PACKAGE_NAME, controller.getPackageName())).isTrue(); + onSetMediaUriCalled = true; + this.uri = uri; + this.extras = extras; + countDownLatch.countDown(); + return RESULT_SUCCESS; + } + + @Override + @NonNull + public ListenableFuture onSetRating( + @NonNull MediaSession session, + @NonNull ControllerInfo controller, + @NonNull String mediaId, + @NonNull Rating rating) { + assertThat(TextUtils.equals(SUPPORT_APP_PACKAGE_NAME, controller.getPackageName())).isTrue(); + onSetRatingCalled = true; + this.mediaId = mediaId; + this.rating = rating; + countDownLatch.countDown(); + return new SessionResult(RESULT_SUCCESS).asFuture(); + } + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionPlayerTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionPlayerTest.java new file mode 100644 index 0000000000..a982860416 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionPlayerTest.java @@ -0,0 +1,410 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.LONG_TIMEOUT_MS; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.TIMEOUT_MS; +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import androidx.annotation.Nullable; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaMetadata; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.device.DeviceInfo; +import com.google.android.exoplayer2.session.vct.common.HandlerThreadTestRule; +import com.google.android.exoplayer2.session.vct.common.MainLooperTestRule; +import com.google.android.exoplayer2.session.vct.common.TestUtils; +import java.util.List; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for the underlying {@link SessionPlayer} of {@link MediaSession}. */ +@RunWith(AndroidJUnit4.class) +@LargeTest +public class MediaSessionPlayerTest { + + @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); + + @Rule + public final HandlerThreadTestRule threadTestRule = + new HandlerThreadTestRule("MediaSessionPlayerTest"); + + @Rule public final RemoteControllerTestRule controllerTestRule = new RemoteControllerTestRule(); + + private MediaSession session; + private MockPlayer player; + private RemoteMediaController controller; + + @Before + public void setUp() throws Exception { + player = + new MockPlayer.Builder() + .setLatchCount(1) + .setApplicationLooper(threadTestRule.getHandler().getLooper()) + .build(); + session = + new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player) + .setSessionCallback( + new MediaSession.SessionCallback() { + @Nullable + @Override + public MediaSession.ConnectResult onConnect( + MediaSession session, MediaSession.ControllerInfo controller) { + if (SUPPORT_APP_PACKAGE_NAME.equals(controller.getPackageName())) { + return super.onConnect(session, controller); + } + return null; + } + }) + .build(); + + // Create a default MediaController in client app. + controller = controllerTestRule.createRemoteController(session.getToken()); + } + + @After + public void cleanUp() { + if (session != null) { + session.release(); + } + } + + @Test + public void play_isCalledByController() throws Exception { + controller.play(); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.playCalled).isTrue(); + } + + @Test + public void pause_isCalledByController() throws Exception { + controller.pause(); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.pauseCalled).isTrue(); + } + + @Test + public void prepare_isCalledByController() throws Exception { + controller.prepare(); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.prepareCalled).isTrue(); + } + + @Test + public void stop_isCalledByController() throws Exception { + controller.stop(); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.stopCalled).isTrue(); + } + + @Test + public void setPlayWhenReady_isCalledByController() throws Exception { + boolean testPlayWhenReady = true; + controller.setPlayWhenReady(testPlayWhenReady); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.setPlayWhenReadyCalled).isTrue(); + assertThat(player.playWhenReady).isEqualTo(testPlayWhenReady); + } + + @Test + public void seekToDefaultPosition_isCalledByController() throws Exception { + controller.seekToDefaultPosition(); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.seekToDefaultPositionCalled).isTrue(); + } + + @Test + public void seekToDefaultPosition_withWindowIndex_isCalledByController() throws Exception { + int windowIndex = 33; + controller.seekToDefaultPosition(windowIndex); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.seekToDefaultPositionWithWindowIndexCalled).isTrue(); + assertThat(player.seekWindowIndex).isEqualTo(windowIndex); + } + + @Test + public void seekTo_isCalledByController() throws Exception { + long seekPositionMs = 12125L; + controller.seekTo(seekPositionMs); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.seekToCalled).isTrue(); + assertThat(player.seekPositionMs).isEqualTo(seekPositionMs); + } + + @Test + public void seekTo_withWindowIndex_isCalledByController() throws Exception { + int windowIndex = 33; + long seekPositionMs = 12125L; + controller.seekTo(windowIndex, seekPositionMs); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.seekToWithWindowIndexCalled).isTrue(); + assertThat(player.seekWindowIndex).isEqualTo(windowIndex); + assertThat(player.seekPositionMs).isEqualTo(seekPositionMs); + } + + @Test + public void setPlaybackSpeed_isCalledByController() throws Exception { + float testSpeed = 1.5f; + controller.setPlaybackSpeed(testSpeed); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.playbackParameters.speed).isEqualTo(testSpeed); + } + + @Test + public void setPlaybackParameters_isCalledByController() throws Exception { + PlaybackParameters testPlaybackParameters = + new PlaybackParameters(/* speed= */ 1.4f, /* pitch= */ 2.3f); + controller.setPlaybackParameters(testPlaybackParameters); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.setPlaybackParametersCalled).isTrue(); + assertThat(player.playbackParameters).isEqualTo(testPlaybackParameters); + } + + @Test + public void setPlaybackParameters_withDefault_isCalledByController() throws Exception { + controller.setPlaybackParameters(PlaybackParameters.DEFAULT); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.setPlaybackParametersCalled).isTrue(); + assertThat(player.playbackParameters).isEqualTo(PlaybackParameters.DEFAULT); + } + + @Test + public void setMediaItems_withResetPosition_isCalledByController() throws Exception { + List items = MediaTestUtils.createConvergedMediaItems(/* size= */ 2); + + controller.setMediaItems(items, /* resetPosition= */ true); + + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.setMediaItemsCalled).isTrue(); + assertThat(player.mediaItems).isEqualTo(items); + assertThat(player.resetPosition).isTrue(); + } + + @Test + public void setMediaItems_withoutResetPosition_isCalledByController() throws Exception { + List items = MediaTestUtils.createConvergedMediaItems(/* size= */ 2); + + controller.setMediaItems(items, /* resetPosition= */ false); + + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.setMediaItemsCalled).isTrue(); + assertThat(player.mediaItems).isEqualTo(items); + assertThat(player.resetPosition).isFalse(); + } + + @Test + public void setMediaItems_withStartWindowAndPosition_isCalledByController() throws Exception { + List items = MediaTestUtils.createConvergedMediaItems(/* size= */ 2); + int startWindowIndex = 1; + long startPositionMs = 1234; + + controller.setMediaItems(items, startWindowIndex, startPositionMs); + + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.setMediaItemsCalled).isTrue(); + assertThat(player.mediaItems).isEqualTo(items); + assertThat(player.startWindowIndex).isEqualTo(startWindowIndex); + assertThat(player.startPositionMs).isEqualTo(startPositionMs); + } + + @Test + public void setMediaItems_withDuplicatedItems_isCalledByController() throws Exception { + int listSize = 4; + List list = MediaTestUtils.createConvergedMediaItems(listSize); + list.set(2, list.get(1)); + controller.setMediaItems(list); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.setMediaItemsCalled).isTrue(); + assertThat(player.mediaItems.size()).isEqualTo(listSize); + for (int i = 0; i < listSize; i++) { + assertThat(player.mediaItems.get(i).mediaId).isEqualTo(list.get(i).mediaId); + } + } + + @Test + public void setMediaItems_withLongPlaylist_isCalledByController() throws Exception { + int listSize = 5000; + // Make client app to generate a long list, and call setMediaItems() with it. + controller.createAndSetFakeMediaItems(listSize); + assertThat(player.countDownLatch.await(LONG_TIMEOUT_MS, MILLISECONDS)).isTrue(); + + assertThat(player.setMediaItemsCalled).isTrue(); + assertThat(player.mediaItems).isNotNull(); + assertThat(player.mediaItems.size()).isEqualTo(listSize); + for (int i = 0; i < listSize; i++) { + // Each item's media ID will be same as its index. + assertThat(player.mediaItems.get(i).mediaId).isEqualTo(TestUtils.getMediaIdInFakeTimeline(i)); + } + } + + @Test + public void setPlaylistMetadata_isCalledByController() throws Exception { + MediaMetadata playlistMetadata = new MediaMetadata.Builder().setTitle("title").build(); + + controller.setPlaylistMetadata(playlistMetadata); + + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.setPlaylistMetadataCalled).isTrue(); + assertThat(player.playlistMetadata).isEqualTo(playlistMetadata); + } + + @Test + public void addMediaItems_isCalledByController() throws Exception { + int index = 1; + int size = 2; + List mediaItems = MediaTestUtils.createConvergedMediaItems(size); + + controller.addMediaItems(index, mediaItems); + + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.addMediaItemsCalled).isTrue(); + assertThat(player.index).isEqualTo(index); + assertThat(player.mediaItems).isEqualTo(mediaItems); + } + + @Test + public void removeMediaItems_isCalledByController() throws Exception { + int fromIndex = 1; + int toIndex = 3; + + controller.removeMediaItems(fromIndex, toIndex); + + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.removeMediaItemsCalled).isTrue(); + assertThat(player.fromIndex).isEqualTo(fromIndex); + assertThat(player.toIndex).isEqualTo(toIndex); + } + + @Test + public void moveMediaItems_isCalledByController() throws Exception { + int fromIndex = 1; + int toIndex = 2; + int newIndex = 3; + + controller.moveMediaItems(fromIndex, toIndex, newIndex); + + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.moveMediaItemsCalled).isTrue(); + assertThat(player.fromIndex).isEqualTo(fromIndex); + assertThat(player.toIndex).isEqualTo(toIndex); + assertThat(player.newIndex).isEqualTo(newIndex); + } + + @Test + public void skipToPreviousItem_isCalledByController() throws Exception { + controller.previous(); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.previousCalled).isTrue(); + } + + @Test + public void skipToNextItem_isCalledByController() throws Exception { + controller.next(); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.nextCalled).isTrue(); + } + + @Test + public void setShuffleModeEnabled_isCalledByController() throws Exception { + boolean testShuffleModeEnabled = true; + controller.setShuffleModeEnabled(testShuffleModeEnabled); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + + assertThat(player.setShuffleModeCalled).isTrue(); + assertThat(player.shuffleModeEnabled).isEqualTo(testShuffleModeEnabled); + } + + @Test + public void setRepeatMode_isCalledByController() throws Exception { + int testRepeatMode = Player.REPEAT_MODE_ALL; + controller.setRepeatMode(testRepeatMode); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + + assertThat(player.setRepeatModeCalled).isTrue(); + assertThat(player.repeatMode).isEqualTo(testRepeatMode); + } + + @Test + public void setVolume_isCalledByController() throws Exception { + float testVolume = .123f; + controller.setVolume(testVolume); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.setVolumeCalled).isTrue(); + assertThat(player.volume).isEqualTo(testVolume); + } + + @Test + public void setDeviceVolume_isCalledByController() throws Exception { + changePlaybackTypeToRemote(); + + int testVolume = 12; + controller.setDeviceVolume(testVolume); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.setDeviceVolumeCalled).isTrue(); + assertThat(player.deviceVolume).isEqualTo(testVolume); + } + + @Test + public void increaseDeviceVolume_isCalledByController() throws Exception { + changePlaybackTypeToRemote(); + + controller.increaseDeviceVolume(); + + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.increaseDeviceVolumeCalled).isTrue(); + } + + @Test + public void decreaseDeviceVolume_isCalledByController() throws Exception { + changePlaybackTypeToRemote(); + + controller.decreaseDeviceVolume(); + + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.decreaseDeviceVolumeCalled).isTrue(); + } + + @Test + public void setDeviceMuted_isCalledByController() throws Exception { + player.deviceMuted = false; + controller.setDeviceMuted(true); + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.setDeviceMutedCalled).isTrue(); + assertThat(player.deviceMuted).isTrue(); + } + + private void changePlaybackTypeToRemote() throws Exception { + threadTestRule + .getHandler() + .postAndSync( + () -> { + player.deviceInfo = + new DeviceInfo( + DeviceInfo.PLAYBACK_TYPE_REMOTE, /* minVolume= */ 0, /* maxVolume= */ 100); + player.notifyDeviceInfoChanged(); + }); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionServiceNotificationTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionServiceNotificationTest.java new file mode 100644 index 0000000000..fd913af788 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionServiceNotificationTest.java @@ -0,0 +1,180 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.Player.STATE_READY; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.MOCK_MEDIA2_SESSION_SERVICE; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.SERVICE_CONNECTION_TIMEOUT_MS; +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Looper; +import androidx.annotation.Nullable; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaMetadata; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.session.MediaLibraryService.MediaLibrarySession.MediaLibrarySessionCallback; +import com.google.android.exoplayer2.session.MediaSession.ControllerInfo; +import com.google.android.exoplayer2.session.vct.common.HandlerThreadTestRule; +import com.google.android.exoplayer2.session.vct.common.MainLooperTestRule; +import com.google.android.exoplayer2.session.vct.common.R; +import com.google.android.exoplayer2.session.vct.common.TestHandler; +import java.util.concurrent.CountDownLatch; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Manual test of {@link MediaSessionService} for showing/removing notification when the playback is + * started/ended. + * + *

This test is a manual test, which means the one who runs this test should keep looking at the + * device and check whether the notification is shown/removed. + */ +@RunWith(AndroidJUnit4.class) +@LargeTest +public class MediaSessionServiceNotificationTest { + private static final long NOTIFICATION_SHOW_TIME_MS = 15000; + + @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); + + @Rule + public final HandlerThreadTestRule threadTestRule = + new HandlerThreadTestRule("MediaSessionServiceNotificationTest"); + + @Rule public final RemoteControllerTestRule controllerTestRule = new RemoteControllerTestRule(); + + private Context context; + private MockPlayer player; + + @Before + public void setUp() { + context = ApplicationProvider.getApplicationContext(); + } + + @Test + @Ignore("Comment out this line and manually run the test.") + public void notification() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + MediaLibrarySessionCallback sessionCallback = + new MediaLibrarySessionCallback() { + @Nullable + @Override + public MediaSession.ConnectResult onConnect( + MediaSession session, ControllerInfo controller) { + if (SUPPORT_APP_PACKAGE_NAME.equals(controller.getPackageName())) { + player = + new MockPlayer.Builder() + .setApplicationLooper(Looper.myLooper()) + .setChangePlayerStateWithTransportControl(true) + .build(); + session.setPlayer(player); + latch.countDown(); + } + return super.onConnect(session, controller); + } + }; + TestServiceRegistry.getInstance().setSessionCallback(sessionCallback); + + // Create a controller to start the service. + controllerTestRule.createRemoteController( + new SessionToken(context, MOCK_MEDIA2_SESSION_SERVICE), + /* waitForConnection= */ true, + /* connectionHints= */ null); + assertThat(latch.await(SERVICE_CONNECTION_TIMEOUT_MS, MILLISECONDS)).isTrue(); + + TestHandler handler = new TestHandler(player.getApplicationLooper()); + handler.postAndSync( + () -> { + // Set current media item. + String mediaId = "testMediaId"; + String title = "Test Song Name"; + Bitmap albumArt = + BitmapFactory.decodeResource(context.getResources(), R.drawable.big_buck_bunny); + // TODO(b/180293668): Set artist, album art, browsable type, playable. + MediaMetadata metadata = new MediaMetadata.Builder().setTitle(title).build(); + player.currentMediaItem = + new MediaItem.Builder().setMediaId(mediaId).setMediaMetadata(metadata).build(); + + // Notification should be shown. Clicking play/pause button will change the player state. + // When playing, the notification will not be removed by swiping horizontally. + // When paused, the notification can be swiped away. + player.notifyPlaybackStateChanged(STATE_READY); + }); + Thread.sleep(NOTIFICATION_SHOW_TIME_MS); + } + + @Test + @Ignore("Comment out this line and manually run the test.") + public void notificationUpdatedWhenCurrentMediaItemChanged() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + MediaLibrarySessionCallback sessionCallback = + new MediaLibrarySessionCallback() { + @Nullable + @Override + public MediaSession.ConnectResult onConnect( + MediaSession session, ControllerInfo controller) { + if (SUPPORT_APP_PACKAGE_NAME.equals(controller.getPackageName())) { + session.setPlayer(player); + latch.countDown(); + } + return super.onConnect(session, controller); + } + }; + TestServiceRegistry.getInstance().setSessionCallback(sessionCallback); + + // Create a controller to start the service. + controllerTestRule.createRemoteController( + new SessionToken(context, MOCK_MEDIA2_SESSION_SERVICE), + /* waitForConnection= */ true, + /* connectionHints= */ null); + + // Set current media item. + Bitmap albumArt = + BitmapFactory.decodeResource(context.getResources(), R.drawable.big_buck_bunny); + // TODO(b/180293668): Set artist, album art, browsable type, playable. + MediaMetadata metadata = new MediaMetadata.Builder().setTitle("Test Song Name").build(); + player.currentMediaItem = + new MediaItem.Builder().setMediaId("testMediaId").setMediaMetadata(metadata).build(); + + player.notifyPlaybackStateChanged(STATE_READY); + // At this point, the notification should be shown. + Thread.sleep(NOTIFICATION_SHOW_TIME_MS); + + // Set a new media item. (current media item is changed) + // TODO(b/180293668): Set artist, album art, browsable type, playable. + MediaMetadata newMetadata = new MediaMetadata.Builder().setTitle("New Song Name").build(); + + MediaItem newItem = + new MediaItem.Builder().setMediaId("New media ID").setMediaMetadata(newMetadata).build(); + player.currentMediaItem = newItem; + + // Calling this should update the notification with the new metadata. + player.notifyMediaItemTransition(newItem, Player.MEDIA_ITEM_TRANSITION_REASON_SEEK); + Thread.sleep(NOTIFICATION_SHOW_TIME_MS); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionServiceTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionServiceTest.java new file mode 100644 index 0000000000..9a3b69310d --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionServiceTest.java @@ -0,0 +1,335 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.NO_RESPONSE_TIMEOUT_MS; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.TIMEOUT_MS; +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.ComponentName; +import android.content.Context; +import android.os.Bundle; +import android.os.Looper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.MediumTest; +import com.google.android.exoplayer2.session.MediaSession.ControllerInfo; +import com.google.android.exoplayer2.session.vct.common.HandlerThreadTestRule; +import com.google.android.exoplayer2.session.vct.common.MainLooperTestRule; +import com.google.android.exoplayer2.session.vct.common.TestUtils; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link MediaSessionService}. */ +@RunWith(AndroidJUnit4.class) +@MediumTest +public class MediaSessionServiceTest { + + @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); + + @Rule + public final HandlerThreadTestRule threadTestRule = + new HandlerThreadTestRule("MediaSessionServiceTest"); + + @Rule public final RemoteControllerTestRule controllerTestRule = new RemoteControllerTestRule(); + + @Rule public final MediaSessionTestRule sessionTestRule = new MediaSessionTestRule(); + + private Context context; + private Looper looper; + private SessionToken token; + + @Before + public void setUp() { + TestServiceRegistry.getInstance().cleanUp(); + context = ApplicationProvider.getApplicationContext(); + looper = threadTestRule.getHandler().getLooper(); + token = + new SessionToken(context, new ComponentName(context, LocalMockMediaSessionService.class)); + } + + @After + public void cleanUp() { + TestServiceRegistry.getInstance().cleanUp(); + } + + /** + * Tests whether {@link MediaSessionService#onGetSession(ControllerInfo)} is called when + * controller tries to connect, with the proper arguments. + */ + @Test + public void onGetSessionIsCalled() throws Exception { + List controllerInfoList = new ArrayList<>(); + CountDownLatch latch = new CountDownLatch(1); + TestServiceRegistry.getInstance() + .setOnGetSessionHandler( + new TestServiceRegistry.OnGetSessionHandler() { + @Override + public MediaSession onGetSession(ControllerInfo controllerInfo) { + controllerInfoList.add(controllerInfo); + latch.countDown(); + return null; + } + }); + + Bundle testHints = new Bundle(); + testHints.putString("test_key", "test_value"); + controllerTestRule.createRemoteController(token, /* waitForConnection= */ false, testHints); + + // onGetSession() should be called. + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(controllerInfoList.get(0).getPackageName()).isEqualTo(SUPPORT_APP_PACKAGE_NAME); + assertThat(TestUtils.equals(controllerInfoList.get(0).getConnectionHints(), testHints)) + .isTrue(); + } + + /** + * Tests whether the controller is connected to the session which is returned from {@link + * MediaSessionService#onGetSession(ControllerInfo)}. Also checks whether the connection hints are + * properly passed to {@link MediaSession.SessionCallback#onConnect(MediaSession, + * ControllerInfo)}. + */ + @Test + public void onGetSession_returnsSession() throws Exception { + List controllerInfoList = new ArrayList<>(); + CountDownLatch latch = new CountDownLatch(1); + + MediaSession testSession = + sessionTestRule.ensureReleaseAfterTest( + new MediaSession.Builder( + context, new MockPlayer.Builder().setApplicationLooper(looper).build()) + .setId("testOnGetSession_returnsSession") + .setSessionCallback( + new MediaSession.SessionCallback() { + @Nullable + @Override + public MediaSession.ConnectResult onConnect( + MediaSession session, ControllerInfo controller) { + controllerInfoList.add(controller); + latch.countDown(); + return new MediaSession.ConnectResult(); + } + }) + .build()); + + TestServiceRegistry.getInstance() + .setOnGetSessionHandler( + new TestServiceRegistry.OnGetSessionHandler() { + @Override + public MediaSession onGetSession(ControllerInfo controllerInfo) { + return testSession; + } + }); + + Bundle testHints = new Bundle(); + testHints.putString("test_key", "test_value"); + RemoteMediaController controller = + controllerTestRule.createRemoteController(token, true, testHints); + + // MediaSession.SessionCallback#onConnect() should be called. + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(controllerInfoList.get(0).getPackageName()).isEqualTo(SUPPORT_APP_PACKAGE_NAME); + assertThat(TestUtils.equals(controllerInfoList.get(0).getConnectionHints(), testHints)) + .isTrue(); + + // The controller should be connected to the right session. + assertThat(controller.getConnectedSessionToken()).isNotEqualTo(token); + assertThat(controller.getConnectedSessionToken()).isEqualTo(testSession.getToken()); + } + + /** + * Tests whether {@link MediaSessionService#onGetSession(ControllerInfo)} can return different + * sessions for different controllers. + */ + @Test + public void onGetSession_returnsDifferentSessions() throws Exception { + List tokens = new ArrayList<>(); + TestServiceRegistry.getInstance() + .setOnGetSessionHandler( + new TestServiceRegistry.OnGetSessionHandler() { + @Override + public MediaSession onGetSession(ControllerInfo controllerInfo) { + MediaSession session = + createMediaSession( + "testOnGetSession_returnsDifferentSessions" + System.currentTimeMillis()); + tokens.add(session.getToken()); + return session; + } + }); + + RemoteMediaController controller1 = + controllerTestRule.createRemoteController(token, true, null); + RemoteMediaController controller2 = + controllerTestRule.createRemoteController(token, true, null); + + assertThat(controller2.getConnectedSessionToken()) + .isNotEqualTo(controller1.getConnectedSessionToken()); + assertThat(controller1.getConnectedSessionToken()).isEqualTo(tokens.get(0)); + assertThat(controller2.getConnectedSessionToken()).isEqualTo(tokens.get(1)); + } + + /** + * Tests whether {@link MediaSessionService#onGetSession(ControllerInfo)} can reject incoming + * connection by returning null. + */ + @Test + public void onGetSession_rejectsConnection() throws Exception { + TestServiceRegistry.getInstance() + .setOnGetSessionHandler( + new TestServiceRegistry.OnGetSessionHandler() { + @Override + public MediaSession onGetSession(ControllerInfo controllerInfo) { + return null; + } + }); + CountDownLatch latch = new CountDownLatch(1); + MediaController controller = + new MediaController.Builder(context) + .setSessionToken(token) + .setControllerCallback( + new MediaController.ControllerCallback() { + @Override + public void onDisconnected(@NonNull MediaController controller) { + latch.countDown(); + } + }) + .setApplicationLooper(looper) + .build(); + + // MediaController2.ControllerCallback#onDisconnected() should be called. + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(controller.getConnectedToken()).isNull(); + threadTestRule.getHandler().postAndSync(controller::release); + } + + @Test + public void allControllersDisconnected_oneSession() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + TestServiceRegistry.getInstance() + .setSessionServiceCallback( + new TestServiceRegistry.SessionServiceCallback() { + @Override + public void onCreated() { + // no-op + } + + @Override + public void onDestroyed() { + latch.countDown(); + } + }); + + RemoteMediaController controller1 = + controllerTestRule.createRemoteController(token, true, null); + RemoteMediaController controller2 = + controllerTestRule.createRemoteController(token, true, null); + controller1.release(); + controller2.release(); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void allControllersDisconnected_multipleSessions() throws Exception { + TestServiceRegistry.getInstance() + .setOnGetSessionHandler( + new TestServiceRegistry.OnGetSessionHandler() { + @Override + public MediaSession onGetSession(ControllerInfo controllerInfo) { + return createMediaSession( + "testAllControllersDisconnected" + System.currentTimeMillis()); + } + }); + CountDownLatch latch = new CountDownLatch(1); + TestServiceRegistry.getInstance() + .setSessionServiceCallback( + new TestServiceRegistry.SessionServiceCallback() { + @Override + public void onCreated() { + // no-op + } + + @Override + public void onDestroyed() { + latch.countDown(); + } + }); + + RemoteMediaController controller1 = + controllerTestRule.createRemoteController(token, true, null); + RemoteMediaController controller2 = + controllerTestRule.createRemoteController(token, true, null); + + controller1.release(); + assertThat(latch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); + + // Service should be closed only when all controllers are closed. + controller2.release(); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + @Test + public void getSessions() throws Exception { + controllerTestRule.createRemoteController( + token, /* waitForConnection= */ true, /* connectionHints= */ null); + MediaSessionService service = TestServiceRegistry.getInstance().getServiceInstance(); + MediaSession session = createMediaSession("testGetSessions"); + service.addSession(session); + List sessions = service.getSessions(); + assertThat(sessions.contains(session)).isTrue(); + assertThat(sessions.size()).isEqualTo(2); + + service.removeSession(session); + sessions = service.getSessions(); + assertThat(sessions.contains(session)).isFalse(); + } + + @Test + public void addSessions_removedWhenClose() throws Exception { + controllerTestRule.createRemoteController( + token, /* waitForConnection= */ true, /* connectionHints= */ null); + MediaSessionService service = TestServiceRegistry.getInstance().getServiceInstance(); + MediaSession session = createMediaSession("testAddSessions_removedWhenClose"); + service.addSession(session); + List sessions = service.getSessions(); + assertThat(sessions.contains(session)).isTrue(); + assertThat(sessions.size()).isEqualTo(2); + + session.release(); + sessions = service.getSessions(); + assertThat(sessions.contains(session)).isFalse(); + } + + private MediaSession createMediaSession(String id) { + return sessionTestRule.ensureReleaseAfterTest( + new MediaSession.Builder( + context, new MockPlayer.Builder().setApplicationLooper(looper).build()) + .setId(id) + .build()); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionTest.java new file mode 100644 index 0000000000..4552ab9fe4 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionTest.java @@ -0,0 +1,378 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.Player.STATE_IDLE; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.LONG_TIMEOUT_MS; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.TIMEOUT_MS; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.Context; +import android.os.Build; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.SystemClock; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.text.TextUtils; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.media.MediaSessionManager; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import com.google.android.exoplayer2.session.MediaSession.SessionCallback; +import com.google.android.exoplayer2.session.vct.common.HandlerThreadTestRule; +import com.google.android.exoplayer2.session.vct.common.MainLooperTestRule; +import com.google.android.exoplayer2.session.vct.common.TestHandler; +import com.google.android.exoplayer2.util.Log; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeoutException; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link MediaSession}. */ +@RunWith(AndroidJUnit4.class) +@LargeTest +public class MediaSessionTest { + + private static final String TAG = "MediaSessionTest"; + + @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); + + @Rule public final HandlerThreadTestRule threadTestRule = new HandlerThreadTestRule(TAG); + + @Rule public final RemoteControllerTestRule controllerTestRule = new RemoteControllerTestRule(); + + @Rule public final MediaSessionTestRule sessionTestRule = new MediaSessionTestRule(); + + private Context context; + private TestHandler handler; + private MediaSession session; + private MockPlayer player; + + @Before + public void setUp() { + context = ApplicationProvider.getApplicationContext(); + handler = threadTestRule.getHandler(); + player = + new MockPlayer.Builder().setLatchCount(1).setApplicationLooper(handler.getLooper()).build(); + + session = + sessionTestRule.ensureReleaseAfterTest( + new MediaSession.Builder(context, player) + .setId(TAG) + .setSessionCallback( + new MediaSession.SessionCallback() { + @Nullable + @Override + public MediaSession.ConnectResult onConnect( + MediaSession session, MediaSession.ControllerInfo controller) { + if (TextUtils.equals( + context.getPackageName(), controller.getPackageName())) { + return super.onConnect(session, controller); + } + return null; + } + }) + .build()); + } + + @Test + public void builder() { + MediaSession.Builder builder; + try { + builder = new MediaSession.Builder(context, null); + assertWithMessage("null player shouldn't be allowed").fail(); + } catch (NullPointerException e) { + // expected. pass-through + } + try { + builder = new MediaSession.Builder(context, player); + builder.setId(null); + assertWithMessage("null id shouldn't be allowed").fail(); + } catch (NullPointerException e) { + // expected. pass-through + } + try { + builder = new MediaSession.Builder(context, player); + builder.setExtras(null); + assertWithMessage("null extras shouldn't be allowed").fail(); + } catch (NullPointerException e) { + // expected. pass-through + } + // Empty string as ID is allowed. + new MediaSession.Builder(context, player).setId("").build().release(); + } + + @Test + public void getPlayer() throws Exception { + assertThat(session.getPlayer()).isEqualTo(player); + } + + @Test + public void setPlayer() throws Exception { + MockPlayer player = + new MockPlayer.Builder().setApplicationLooper(this.player.getApplicationLooper()).build(); + session.setPlayer(player); + assertThat(session.getPlayer()).isEqualTo(player); + } + + @Test + public void setPlayer_withSamePlayerInstance() throws Exception { + session.setPlayer(player); + assertThat(session.getPlayer()).isEqualTo(player); + } + + @Test + public void setPlayer_withDifferentLooper_throwsIAE() throws Exception { + MockPlayer player = + new MockPlayer.Builder().setApplicationLooper(Looper.getMainLooper()).build(); + try { + session.setPlayer(player); + assertWithMessage("IAE is expected").fail(); + } catch (IllegalArgumentException unused) { + // expected + } + } + + /** Test potential deadlock for calls between controller and session. */ + @Test + @LargeTest + public void deadlock() throws Exception { + handler.postAndSync( + () -> { + session.release(); + session = null; + }); + + HandlerThread testThread = new HandlerThread("deadlock"); + testThread.start(); + TestHandler testHandler = new TestHandler(testThread.getLooper()); + try { + MockPlayer player = + new MockPlayer.Builder().setApplicationLooper(testThread.getLooper()).build(); + handler.postAndSync( + () -> { + session = new MediaSession.Builder(context, player).setId("testDeadlock").build(); + }); + RemoteMediaController controller = + controllerTestRule.createRemoteController(session.getToken()); + // This may hang if deadlock happens. + testHandler.postAndSync( + () -> { + int state = STATE_IDLE; + for (int i = 0; i < 100; i++) { + Log.d(TAG, "testDeadlock for-loop started: index=" + i); + long startTime = SystemClock.elapsedRealtime(); + + // triggers call from session to controller. + player.notifyPlaybackStateChanged(state); + long endTime = SystemClock.elapsedRealtime(); + Log.d(TAG, "1) Time spent on API call(ms): " + (endTime - startTime)); + + // triggers call from controller to session. + startTime = endTime; + controller.play(); + endTime = SystemClock.elapsedRealtime(); + Log.d(TAG, "2) Time spent on API call(ms): " + (endTime - startTime)); + + // Repeat above + startTime = endTime; + player.notifyPlaybackStateChanged(state); + endTime = SystemClock.elapsedRealtime(); + Log.d(TAG, "3) Time spent on API call(ms): " + (endTime - startTime)); + + startTime = endTime; + controller.pause(); + endTime = SystemClock.elapsedRealtime(); + Log.d(TAG, "4) Time spent on API call(ms): " + (endTime - startTime)); + + startTime = endTime; + player.notifyPlaybackStateChanged(state); + endTime = SystemClock.elapsedRealtime(); + Log.d(TAG, "5) Time spent on API call(ms): " + (endTime - startTime)); + + startTime = endTime; + controller.seekTo(0); + endTime = SystemClock.elapsedRealtime(); + Log.d(TAG, "6) Time spent on API call(ms): " + (endTime - startTime)); + + startTime = endTime; + player.notifyPlaybackStateChanged(state); + endTime = SystemClock.elapsedRealtime(); + Log.d(TAG, "7) Time spent on API call(ms): " + (endTime - startTime)); + + startTime = endTime; + controller.next(); + endTime = SystemClock.elapsedRealtime(); + Log.d(TAG, "8) Time spent on API call(ms): " + (endTime - startTime)); + + startTime = endTime; + player.notifyPlaybackStateChanged(state); + endTime = SystemClock.elapsedRealtime(); + Log.d(TAG, "9) Time spent on API call(ms): " + (endTime - startTime)); + + startTime = endTime; + controller.previous(); + endTime = SystemClock.elapsedRealtime(); + Log.d(TAG, "10) Time spent on API call(ms): " + (endTime - startTime)); + } + }, + LONG_TIMEOUT_MS); + } finally { + if (session != null) { + handler.postAndSync( + () -> { + // Clean up here because sessionHandler will be removed afterwards. + session.release(); + session = null; + }); + } + + if (Build.VERSION.SDK_INT >= 18) { + testThread.quitSafely(); + } else { + testThread.quit(); + } + } + } + + @Test + public void creatingTwoSessionWithSameId() { + String sessionId = "testSessionId"; + MediaSession session = + new MediaSession.Builder( + context, new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build()) + .setId(sessionId) + .build(); + + MediaSession.Builder builderWithSameId = + new MediaSession.Builder( + context, new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build()); + try { + builderWithSameId.setId(sessionId).build(); + assertWithMessage( + "Creating a new session with the same ID in a process should not be allowed") + .fail(); + } catch (IllegalStateException e) { + // expected. pass-through + } + + session.release(); + // Creating a new session with ID of the closed session is okay. + MediaSession sessionWithSameId = builderWithSameId.build(); + sessionWithSameId.release(); + } + + @Test + public void sendCustomCommand_onConnect() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + SessionCommand testCommand = new SessionCommand("test", null); + SessionCallback testSessionCallback = + new SessionCallback() { + @Nullable + @Override + public MediaSession.ConnectResult onConnect( + MediaSession session, MediaSession.ControllerInfo controller) { + Future result = session.sendCustomCommand(controller, testCommand, null); + try { + // The controller is not connected yet. + assertThat(result.get(TIMEOUT_MS, MILLISECONDS).resultCode) + .isEqualTo(SessionResult.RESULT_ERROR_SESSION_DISCONNECTED); + } catch (ExecutionException | InterruptedException | TimeoutException e) { + assertWithMessage("Fail to get result of the returned future.").fail(); + } + return super.onConnect(session, controller); + } + + @Override + public void onPostConnect( + @NonNull MediaSession session, @NonNull MediaSession.ControllerInfo controller) { + Future result = session.sendCustomCommand(controller, testCommand, null); + try { + // The controller is connected but doesn't implement onCustomCommand. + assertThat(result.get(TIMEOUT_MS, MILLISECONDS).resultCode) + .isEqualTo(SessionResult.RESULT_ERROR_NOT_SUPPORTED); + } catch (ExecutionException | InterruptedException | TimeoutException e) { + assertWithMessage("Fail to get result of the returned future.").fail(); + } + latch.countDown(); + } + }; + MediaSession session = + sessionTestRule.ensureReleaseAfterTest( + new MediaSession.Builder(context, player) + .setSessionCallback(testSessionCallback) + .build()); + controllerTestRule.createRemoteController(session.getToken()); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + + /** Test {@link MediaSession#getSessionCompatToken()}. */ + @Test + public void getSessionCompatToken_returnsCompatibleWithMediaControllerCompat() throws Exception { + String expectedControllerCompatPackageName = + (21 <= Build.VERSION.SDK_INT && Build.VERSION.SDK_INT < 24) + ? MediaSessionManager.RemoteUserInfo.LEGACY_CONTROLLER + : context.getPackageName(); + MediaSession session = + sessionTestRule.ensureReleaseAfterTest( + new MediaSession.Builder(context, player) + .setId("getSessionCompatToken_returnsCompatibleWithMediaControllerCompat") + .setSessionCallback( + new SessionCallback() { + @Nullable + @Override + public MediaSession.ConnectResult onConnect( + MediaSession session, MediaSession.ControllerInfo controller) { + if (TextUtils.equals( + expectedControllerCompatPackageName, controller.getPackageName())) { + return super.onConnect(session, controller); + } + return null; + } + }) + .build()); + MediaSessionCompat.Token token = session.getSessionCompatToken(); + MediaControllerCompat controllerCompat = new MediaControllerCompat(context, token); + CountDownLatch sessionReadyLatch = new CountDownLatch(1); + controllerCompat.registerCallback( + new MediaControllerCompat.Callback() { + @Override + public void onSessionReady() { + sessionReadyLatch.countDown(); + } + }, + handler); + assertThat(sessionReadyLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + + long testSeekPositionMs = 1234; + controllerCompat.getTransportControls().seekTo(testSeekPositionMs); + + assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(player.seekToCalled).isTrue(); + assertThat(player.seekPositionMs).isEqualTo(testSeekPositionMs); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionTestRule.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionTestRule.java new file mode 100644 index 0000000000..9fd1c38b9b --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaSessionTestRule.java @@ -0,0 +1,58 @@ +/* + * Copyright 2021 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.session; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +/** TestRule for releasing {@link MediaSession} instances after use. */ +public class MediaSessionTestRule implements TestRule { + private final List sessions; + + MediaSessionTestRule() { + sessions = new CopyOnWriteArrayList<>(); + } + + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + try { + base.evaluate(); + } finally { + cleanUpSessions(); + } + } + }; + } + + /** Ensures that release() is called after the test. */ + public T ensureReleaseAfterTest(T session) { + sessions.add(session); + return session; + } + + private void cleanUpSessions() { + for (int i = 0; i < sessions.size(); i++) { + sessions.get(i).release(); + } + sessions.clear(); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaUtilsTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaUtilsTest.java new file mode 100644 index 0000000000..c2fc3a84f7 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MediaUtilsTest.java @@ -0,0 +1,433 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.session; + +import static android.support.v4.media.session.MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS; +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.os.Bundle; +import android.os.Parcel; +import android.service.media.MediaBrowserService; +import android.support.v4.media.MediaBrowserCompat; +import android.support.v4.media.MediaDescriptionCompat; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.RatingCompat; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import android.text.TextUtils; +import androidx.media.AudioAttributesCompat; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.HeartRating; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaMetadata; +import com.google.android.exoplayer2.PercentageRating; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Rating; +import com.google.android.exoplayer2.StarRating; +import com.google.android.exoplayer2.ThumbRating; +import com.google.android.exoplayer2.audio.AudioAttributes; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link MediaUtils}. */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public final class MediaUtilsTest { + + private Context context; + + @Before + public void setUp() { + context = ApplicationProvider.getApplicationContext(); + } + + @Test + public void convertToBrowserItem() { + String mediaId = "testId"; + CharSequence trackTitle = "testTitle"; + MediaItem mediaItem = + new MediaItem.Builder() + .setMediaId(mediaId) + .setMediaMetadata(new MediaMetadata.Builder().setTitle(trackTitle).build()) + .build(); + + MediaBrowserCompat.MediaItem browserItem = MediaUtils.convertToBrowserItem(mediaItem); + + assertThat(browserItem.getDescription()).isNotNull(); + assertThat(browserItem.getDescription().getMediaId()).isEqualTo(mediaId); + assertThat(TextUtils.equals(browserItem.getDescription().getTitle(), trackTitle)).isTrue(); + } + + @Test + public void convertToMediaItem_browserItemToMediaItem() { + String mediaId = "testId"; + String title = "testTitle"; + MediaDescriptionCompat descriptionCompat = + new MediaDescriptionCompat.Builder().setMediaId(mediaId).setTitle(title).build(); + MediaBrowserCompat.MediaItem browserItem = + new MediaBrowserCompat.MediaItem(descriptionCompat, /* flags= */ 0); + + MediaItem mediaItem = MediaUtils.convertToMediaItem(browserItem); + assertThat(mediaItem.mediaId).isEqualTo(mediaId); + assertThat(mediaItem.mediaMetadata.title).isEqualTo(title); + } + + @Test + public void convertToMediaItem_queueItemToMediaItem() { + String mediaId = "testMediaId"; + String title = "testTitle"; + MediaDescriptionCompat descriptionCompat = + new MediaDescriptionCompat.Builder().setMediaId(mediaId).setTitle(title).build(); + MediaSessionCompat.QueueItem queueItem = + new MediaSessionCompat.QueueItem(descriptionCompat, /* id= */ 1); + MediaItem mediaItem = MediaUtils.convertToMediaItem(queueItem); + assertThat(mediaItem.mediaId).isEqualTo(mediaId); + assertThat(mediaItem.mediaMetadata.title.toString()).isEqualTo(title); + } + + @Test + public void convertToMediaItem_metadataCompatToMediaItem() { + MediaMetadataCompat metadataCompat = null; + LegacyMediaItem mediaItem = + MediaUtils.convertToLegacyMediaItem(metadataCompat, RatingCompat.RATING_3_STARS); + assertThat(mediaItem).isNull(); + + String mediaId = "testId"; + String displaySubtitle = "testDisplaySubtitle"; + String displayDescription = "testDisplayDescription"; + String title = "testTitle"; + String iconUri = "testIconUri"; + String mediaUri = "testMediaUri"; + + metadataCompat = + new MediaMetadataCompat.Builder() + .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId) + .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, displayDescription) + .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, iconUri) + .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, mediaUri) + .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, displaySubtitle) + .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) + .build(); + mediaItem = MediaUtils.convertToLegacyMediaItem(metadataCompat, RatingCompat.RATING_3_STARS); + assertThat(mediaItem).isNotNull(); + assertThat(mediaItem.getMediaId()).isEqualTo(mediaId); + LegacyMediaMetadata metadata = mediaItem.getMetadata(); + assertThat(metadata).isNotNull(); + assertThat(metadata.getString(LegacyMediaMetadata.METADATA_KEY_DISPLAY_DESCRIPTION)) + .isEqualTo(displayDescription); + assertThat(metadata.getString(LegacyMediaMetadata.METADATA_KEY_DISPLAY_ICON_URI)) + .isEqualTo(iconUri); + assertThat(metadata.getString(LegacyMediaMetadata.METADATA_KEY_MEDIA_URI)).isEqualTo(mediaUri); + assertThat(metadata.getString(LegacyMediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE)) + .isEqualTo(displaySubtitle); + assertThat(metadata.getString(LegacyMediaMetadata.METADATA_KEY_TITLE)).isEqualTo(title); + } + + @Test + public void convertToBrowserItemList() { + int size = 3; + List mediaItems = MediaTestUtils.createConvergedMediaItems(size); + List browserItems = + MediaUtils.convertToBrowserItemList(mediaItems); + assertThat(browserItems).hasSize(size); + for (int i = 0; i < size; ++i) { + assertThat(browserItems.get(i).getMediaId()).isEqualTo(mediaItems.get(i).mediaId); + } + } + + @Test + public void convertBrowserItemListToMediaItemList() { + int size = 3; + List browserItems = MediaTestUtils.createBrowserItems(size); + List mediaItems = MediaUtils.convertBrowserItemListToMediaItemList(browserItems); + assertThat(mediaItems).hasSize(size); + for (int i = 0; i < size; ++i) { + assertThat(mediaItems.get(i).mediaId).isEqualTo(browserItems.get(i).getMediaId()); + } + } + + @Test + public void convertToMediaItemList_queueItemToMediaItem() { + int size = 3; + List queueItems = MediaTestUtils.createQueueItems(size); + List mediaItems = MediaUtils.convertQueueItemListToMediaItemList(queueItems); + assertThat(mediaItems).hasSize(queueItems.size()); + for (int i = 0; i < size; ++i) { + assertThat(mediaItems.get(i).mediaId) + .isEqualTo(queueItems.get(i).getDescription().getMediaId()); + } + } + + @Test + public void convertToQueueItemList() { + int size = 3; + List mediaItems = MediaTestUtils.createConvergedMediaItems(size); + List queueItems = MediaUtils.convertToQueueItemList(mediaItems); + assertThat(queueItems).hasSize(mediaItems.size()); + for (int i = 0; i < size; ++i) { + assertThat(queueItems.get(i).getDescription().getMediaId()) + .isEqualTo(mediaItems.get(i).mediaId); + } + } + + @Test + public void createMediaDescriptionCompat() { + String mediaId = "testId"; + MediaDescriptionCompat descriptionCompat = MediaUtils.createMediaDescriptionCompat(null); + assertThat(descriptionCompat).isNull(); + + descriptionCompat = MediaUtils.createMediaDescriptionCompat(mediaId); + assertThat(descriptionCompat.getMediaId()).isEqualTo(mediaId); + } + + @Test + public void convertToQueueItemId() { + assertThat(MediaUtils.convertToQueueItemId(C.INDEX_UNSET)) + .isEqualTo(MediaSessionCompat.QueueItem.UNKNOWN_ID); + assertThat(MediaUtils.convertToQueueItemId(100)).isEqualTo(100); + } + + @Test + public void truncateListBySize() { + List bundleList = new ArrayList<>(); + Bundle testBundle = new Bundle(); + testBundle.putString("key", "value"); + + Parcel p = Parcel.obtain(); + p.writeParcelable(testBundle, 0); + int bundleSize = p.dataSize(); + p.recycle(); + + bundleList.addAll(Collections.nCopies(10, testBundle)); + + assertThat(MediaUtils.truncateListBySize(null, 1)).isNull(); + for (int i = 0; i < 5; i++) { + assertThat(MediaUtils.truncateListBySize(bundleList, bundleSize * i + 1)).hasSize(i); + } + } + + @Test + public void convertToMediaMetadata_withoutTitle() { + assertThat(MediaUtils.convertToMediaMetadata(null)).isEqualTo(MediaMetadata.EMPTY); + } + + @Test + public void convertToMediaMetadata_withTitle() { + CharSequence title = "title"; + assertThat(MediaUtils.convertToMediaMetadata(title).title).isEqualTo(title); + } + + @Test + public void convertBetweenRatingAndRatingCompat() { + assertRatingEquals(MediaUtils.convertToRating(null), MediaUtils.convertToRatingCompat(null)); + assertRatingEquals( + MediaUtils.convertToRating(RatingCompat.newUnratedRating(RatingCompat.RATING_NONE)), + MediaUtils.convertToRatingCompat(null)); + assertRatingEquals( + MediaUtils.convertToRating(RatingCompat.newUnratedRating(RatingCompat.RATING_HEART)), + MediaUtils.convertToRatingCompat(new HeartRating())); + assertRatingEquals( + MediaUtils.convertToRating(RatingCompat.newHeartRating(true)), + MediaUtils.convertToRatingCompat(new HeartRating(true))); + assertRatingEquals( + MediaUtils.convertToRating(RatingCompat.newThumbRating(false)), + MediaUtils.convertToRatingCompat(new ThumbRating(false))); + assertRatingEquals( + MediaUtils.convertToRating(RatingCompat.newThumbRating(false)), + MediaUtils.convertToRatingCompat(new ThumbRating(false))); + assertRatingEquals( + MediaUtils.convertToRating(RatingCompat.newStarRating(RatingCompat.RATING_3_STARS, 1f)), + MediaUtils.convertToRatingCompat(new StarRating(3, 1f))); + assertRatingEquals( + MediaUtils.convertToRating(RatingCompat.newStarRating(RatingCompat.RATING_4_STARS, 0f)), + MediaUtils.convertToRatingCompat(new StarRating(4, 0f))); + assertRatingEquals( + MediaUtils.convertToRating(RatingCompat.newStarRating(RatingCompat.RATING_5_STARS, 5f)), + MediaUtils.convertToRatingCompat(new StarRating(5, 5f))); + assertRatingEquals( + MediaUtils.convertToRating(RatingCompat.newPercentageRating(80f)), + MediaUtils.convertToRatingCompat(new PercentageRating(80f))); + } + + void assertRatingEquals(Rating rating, RatingCompat ratingCompat) { + if (rating == null && ratingCompat == null) { + return; + } + assertThat(rating.isRated()).isEqualTo(ratingCompat.isRated()); + if (rating instanceof HeartRating) { + assertThat(ratingCompat.getRatingStyle()).isEqualTo(RatingCompat.RATING_HEART); + assertThat(((HeartRating) rating).isHeart()).isEqualTo(ratingCompat.hasHeart()); + } else if (rating instanceof ThumbRating) { + assertThat(ratingCompat.getRatingStyle()).isEqualTo(RatingCompat.RATING_THUMB_UP_DOWN); + assertThat(((ThumbRating) rating).isThumbsUp()).isEqualTo(ratingCompat.isThumbUp()); + } else if (rating instanceof StarRating) { + StarRating starRating = (StarRating) rating; + switch (starRating.getMaxStars()) { + case 3: + assertThat(ratingCompat.getRatingStyle()).isEqualTo(RatingCompat.RATING_3_STARS); + break; + case 4: + assertThat(ratingCompat.getRatingStyle()).isEqualTo(RatingCompat.RATING_4_STARS); + break; + case 5: + assertThat(ratingCompat.getRatingStyle()).isEqualTo(RatingCompat.RATING_5_STARS); + break; + default: // fall out + } + assertThat(starRating.getStarRating()).isEqualTo(ratingCompat.getStarRating()); + } else if (rating instanceof PercentageRating) { + assertThat(ratingCompat.getRatingStyle()).isEqualTo(RatingCompat.RATING_PERCENTAGE); + assertThat(((PercentageRating) rating).getPercent()) + .isEqualTo(ratingCompat.getPercentRating()); + } + } + + @Test + public void convertToLibraryParams() { + assertThat(MediaUtils.convertToLibraryParams(context, null)).isNull(); + Bundle rootHints = new Bundle(); + rootHints.putString("key", "value"); + rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_OFFLINE, true); + rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); + rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_SUGGESTED, true); + + MediaLibraryService.LibraryParams params = + MediaUtils.convertToLibraryParams(context, rootHints); + assertThat(params.offline).isTrue(); + assertThat(params.recent).isTrue(); + assertThat(params.suggested).isTrue(); + assertThat(params.extras.getString("key")).isEqualTo("value"); + } + + @Test + public void convertToRootHints() { + assertThat(MediaUtils.convertToRootHints(null)).isNull(); + Bundle extras = new Bundle(); + extras.putString("key", "value"); + MediaLibraryService.LibraryParams param = + new MediaLibraryService.LibraryParams.Builder() + .setOffline(true) + .setRecent(true) + .setSuggested(true) + .setExtras(extras) + .build(); + Bundle rootHints = MediaUtils.convertToRootHints(param); + assertThat(rootHints.getBoolean(MediaBrowserService.BrowserRoot.EXTRA_OFFLINE)).isTrue(); + assertThat(rootHints.getBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT)).isTrue(); + assertThat(rootHints.getBoolean(MediaBrowserService.BrowserRoot.EXTRA_SUGGESTED)).isTrue(); + assertThat(rootHints.getString("key")).isEqualTo("value"); + } + + @Test + public void removeNullElements() { + List strings = new ArrayList<>(); + strings.add("str1"); + strings.add(null); + strings.add("str2"); + strings.add(null); + assertThat(MediaUtils.removeNullElements(strings)).containsExactly("str1", "str2"); + } + + @Test + public void convertToSessionCommands() { + PlaybackStateCompat playbackState = + new PlaybackStateCompat.Builder() + .addCustomAction("action", "name", /* icon= */ 100) + .build(); + SessionCommands sessionCommands = MediaUtils.convertToSessionCommands(playbackState); + assertThat(sessionCommands.contains(new SessionCommand("action", /* extras= */ null))).isTrue(); + } + + @Test + public void convertToPlayerCommands() { + long sessionFlags = FLAG_HANDLES_QUEUE_COMMANDS; + Player.Commands playerCommands = MediaUtils.convertToPlayerCommands(sessionFlags); + assertThat(playerCommands.contains(Player.COMMAND_GET_MEDIA_ITEMS)).isTrue(); + } + + @Test + public void convertToCustomLayout() { + assertThat(MediaUtils.convertToCustomLayout(null)).isEmpty(); + + String extraKey = "key"; + String extraValue = "value"; + String actionStr = "action"; + String displayName = "display_name"; + int iconRes = 21; + + Bundle extras = new Bundle(); + extras.putString(extraKey, extraValue); + + PlaybackStateCompat.CustomAction action = + new PlaybackStateCompat.CustomAction.Builder(actionStr, displayName, iconRes) + .setExtras(extras) + .build(); + + PlaybackStateCompat state = + new PlaybackStateCompat.Builder() + .setState( + PlaybackStateCompat.STATE_NONE, + /* position= */ 0, + /* playbackSpeed= */ 1, + /* updateTime= */ 100) + .addCustomAction(action) + .build(); + + List buttons = MediaUtils.convertToCustomLayout(state); + assertThat(buttons).hasSize(1); + CommandButton button = buttons.get(0); + assertThat(button.displayName.toString()).isEqualTo(displayName); + assertThat(button.enabled).isTrue(); + assertThat(button.iconResId).isEqualTo(iconRes); + assertThat(button.sessionCommand.customAction).isEqualTo(actionStr); + assertThat(button.sessionCommand.customExtras.getString(extraKey)).isEqualTo(extraValue); + } + + @Test + public void convertToAudioAttributes() { + assertThat(MediaUtils.convertToAudioAttributes((AudioAttributesCompat) null)) + .isSameInstanceAs(AudioAttributes.DEFAULT); + assertThat(MediaUtils.convertToAudioAttributes((MediaControllerCompat.PlaybackInfo) null)) + .isSameInstanceAs(AudioAttributes.DEFAULT); + + int contentType = AudioAttributesCompat.CONTENT_TYPE_MUSIC; + int flags = AudioAttributesCompat.FLAG_AUDIBILITY_ENFORCED; + int usage = AudioAttributesCompat.USAGE_MEDIA; + AudioAttributesCompat aaCompat = + new AudioAttributesCompat.Builder() + .setContentType(contentType) + .setFlags(flags) + .setUsage(usage) + .build(); + AudioAttributes aa = + new AudioAttributes.Builder() + .setContentType(contentType) + .setFlags(flags) + .setUsage(usage) + .build(); + assertThat(MediaUtils.convertToAudioAttributes(aaCompat)).isEqualTo(aa); + assertThat(MediaUtils.convertToAudioAttributesCompat(aa)).isEqualTo(aaCompat); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MockPlayerTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MockPlayerTest.java new file mode 100644 index 0000000000..6573d55356 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/MockPlayerTest.java @@ -0,0 +1,233 @@ +/* + * Copyright 2021 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.session; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaMetadata; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link MockPlayer}. */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class MockPlayerTest { + + private MockPlayer player; + + @Before + public void setUp() { + player = new MockPlayer.Builder().build(); + } + + @Test + public void play() { + player.play(); + assertThat(player.playCalled).isTrue(); + } + + @Test + public void pause() { + player.pause(); + assertThat(player.pauseCalled).isTrue(); + } + + @Test + public void prepare() { + player.prepare(); + assertThat(player.prepareCalled).isTrue(); + } + + @Test + public void stop() { + player.stop(); + assertThat(player.stopCalled).isTrue(); + } + + @Test + public void release() { + player.release(); + assertThat(player.releaseCalled).isTrue(); + } + + @Test + public void setPlayWhenReady() { + boolean testPlayWhenReady = false; + player.setPlayWhenReady(testPlayWhenReady); + assertThat(player.setPlayWhenReadyCalled).isTrue(); + } + + @Test + public void seekTo() { + long pos = 1004L; + player.seekTo(pos); + assertThat(player.seekToCalled).isTrue(); + assertThat(player.seekPositionMs).isEqualTo(pos); + } + + @Test + public void setPlaybackParameters() { + PlaybackParameters playbackParameters = new PlaybackParameters(/* speed= */ 1.5f); + player.setPlaybackParameters(playbackParameters); + assertThat(player.setPlaybackParametersCalled).isTrue(); + assertThat(player.playbackParameters).isEqualTo(playbackParameters); + } + + @Test + public void setPlaybackSpeed() { + float speed = 1.5f; + player.setPlaybackSpeed(speed); + assertThat(player.setPlaybackSpeedCalled).isTrue(); + assertThat(player.playbackParameters.speed).isEqualTo(speed); + } + + @Test + public void setMediaItems() { + List list = MediaTestUtils.createConvergedMediaItems(/* size= */ 2); + player.setMediaItems(list); + assertThat(player.setMediaItemsCalled).isTrue(); + assertThat(player.mediaItems).isEqualTo(list); + } + + @Test + public void setMediaItems_withDuplicatedItems() { + List list = MediaTestUtils.createConvergedMediaItems(/* size= */ 4); + list.set(2, list.get(1)); + player.setMediaItems(list); + assertThat(player.setMediaItemsCalled).isTrue(); + assertThat(player.mediaItems).isEqualTo(list); + } + + @Test + public void setPlaylistMetadata() { + MediaMetadata playlistMetadata = new MediaMetadata.Builder().setTitle("title").build(); + + player.setPlaylistMetadata(playlistMetadata); + + assertThat(player.setPlaylistMetadataCalled).isTrue(); + assertThat(player.playlistMetadata).isSameInstanceAs(playlistMetadata); + } + + @Test + public void addMediaItems() { + int index = 1; + int size = 2; + List mediaItems = MediaTestUtils.createConvergedMediaItems(size); + + player.addMediaItems(index, mediaItems); + + assertThat(player.addMediaItemsCalled).isTrue(); + assertThat(player.index).isEqualTo(index); + assertThat(player.mediaItems).isSameInstanceAs(mediaItems); + } + + @Test + public void removeMediaItems() { + int fromIndex = 1; + int toIndex = 3; + + player.removeMediaItems(fromIndex, toIndex); + + assertThat(player.removeMediaItemsCalled).isTrue(); + assertThat(player.fromIndex).isEqualTo(fromIndex); + assertThat(player.toIndex).isEqualTo(toIndex); + } + + @Test + public void moveMediaItems() { + int fromIndex = 1; + int toIndex = 2; + int newIndex = 3; + + player.moveMediaItems(fromIndex, toIndex, newIndex); + + assertThat(player.moveMediaItemsCalled).isTrue(); + assertThat(player.fromIndex).isEqualTo(fromIndex); + assertThat(player.toIndex).isEqualTo(toIndex); + assertThat(player.newIndex).isEqualTo(newIndex); + } + + @Test + public void skipToPreviousItem() { + player.previous(); + assertThat(player.previousCalled).isTrue(); + } + + @Test + public void skipToNextItem() { + player.next(); + assertThat(player.nextCalled).isTrue(); + } + + @Test + public void setShuffleModeEnabled() { + boolean testShuffleModeEnabled = true; + player.setShuffleModeEnabled(testShuffleModeEnabled); + assertThat(player.setShuffleModeCalled).isTrue(); + assertThat(player.shuffleModeEnabled).isEqualTo(testShuffleModeEnabled); + } + + @Test + public void setRepeatMode() { + int testRepeatMode = Player.REPEAT_MODE_ALL; + player.setRepeatMode(testRepeatMode); + assertThat(player.setRepeatModeCalled).isTrue(); + assertThat(player.repeatMode).isEqualTo(testRepeatMode); + } + + @Test + public void setVolume() { + float testVolume = .123f; + player.setVolume(testVolume); + assertThat(player.setVolumeCalled).isTrue(); + assertThat(player.volume).isEqualTo(testVolume); + } + + @Test + public void setDeviceVolume() { + int testVolume = 12; + player.setDeviceVolume(testVolume); + assertThat(player.setDeviceVolumeCalled).isTrue(); + assertThat(player.deviceVolume).isEqualTo(testVolume); + } + + @Test + public void increaseDeviceVolume() { + player.increaseDeviceVolume(); + assertThat(player.increaseDeviceVolumeCalled).isTrue(); + } + + @Test + public void decreaseDeviceVolume() { + player.decreaseDeviceVolume(); + assertThat(player.decreaseDeviceVolumeCalled).isTrue(); + } + + @Test + public void setDeviceMuted() { + player.deviceMuted = false; + player.setDeviceMuted(true); + assertThat(player.setDeviceMutedCalled).isTrue(); + assertThat(player.deviceMuted).isTrue(); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/RemoteControllerTestRule.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/RemoteControllerTestRule.java new file mode 100644 index 0000000000..715bd53030 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/RemoteControllerTestRule.java @@ -0,0 +1,94 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.session; + +import static com.google.common.truth.Truth.assertWithMessage; + +import android.content.Context; +import android.os.Bundle; +import android.os.RemoteException; +import androidx.annotation.NonNull; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.exoplayer2.util.Log; +import java.util.ArrayList; +import java.util.List; +import org.junit.rules.ExternalResource; + +/** TestRule for managing {@link RemoteMediaController} instances. */ +public final class RemoteControllerTestRule extends ExternalResource { + + private static final String TAG = "RemoteControllerTestRule"; + + private Context context; + private final List controllers = new ArrayList<>(); + + @Override + protected void before() { + context = ApplicationProvider.getApplicationContext(); + } + + @Override + protected void after() { + Exception exception = null; + for (RemoteMediaController controller : controllers) { + try { + controller.cleanUp(); + } catch (Exception e) { + exception = e; + Log.e(TAG, "Exception thrown while cleanUp()", e); + } + } + if (exception != null) { + assertWithMessage("An exception thrown: " + exception).fail(); + } + } + + /** + * Creates {@link RemoteMediaController} from {@link SessionToken} with default options waiting + * for connection. + */ + @NonNull + public RemoteMediaController createRemoteController(@NonNull SessionToken token) + throws RemoteException { + return createRemoteController( + token, /* waitForConnection= */ true, /* connectionHints= */ null); + } + + /** Creates {@link RemoteMediaController} from {@link SessionToken}. */ + @NonNull + public RemoteMediaController createRemoteController( + @NonNull SessionToken token, boolean waitForConnection, Bundle connectionHints) + throws RemoteException { + RemoteMediaController controller = + new RemoteMediaController(context, token, connectionHints, waitForConnection); + controllers.add(controller); + return controller; + } + + /** + * Creates {@link RemoteMediaBrowser} from {@link SessionToken} with default options waiting for + * connection. + */ + @NonNull + public RemoteMediaBrowser createRemoteBrowser(@NonNull SessionToken token) + throws RemoteException { + RemoteMediaBrowser browser = + new RemoteMediaBrowser( + context, token, /* waitForConnection= */ true, /* connectionHints= */ null); + controllers.add(browser); + return browser; + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/RemoteMediaBrowserCompatTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/RemoteMediaBrowserCompatTest.java new file mode 100644 index 0000000000..c9e653e576 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/RemoteMediaBrowserCompatTest.java @@ -0,0 +1,60 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.MOCK_MEDIA2_LIBRARY_SERVICE; +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import com.google.android.exoplayer2.session.vct.common.MainLooperTestRule; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link RemoteMediaBrowserCompat}. */ +@RunWith(AndroidJUnit4.class) +public class RemoteMediaBrowserCompatTest { + + @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); + + private RemoteMediaBrowserCompat remoteBrowserCompat; + + @Before + public void setUp() throws Exception { + remoteBrowserCompat = + new RemoteMediaBrowserCompat( + ApplicationProvider.getApplicationContext(), MOCK_MEDIA2_LIBRARY_SERVICE); + } + + @After + public void cleanUp() throws Exception { + if (remoteBrowserCompat != null) { + remoteBrowserCompat.cleanUp(); + } + } + + @Test + @SmallTest + public void connect() throws Exception { + remoteBrowserCompat.connect(/* waitForConnection= */ true); + assertThat(remoteBrowserCompat.isConnected()).isTrue(); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/RemoteMediaControllerCompatTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/RemoteMediaControllerCompatTest.java new file mode 100644 index 0000000000..b8d68f2487 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/RemoteMediaControllerCompatTest.java @@ -0,0 +1,90 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.DEFAULT_TEST_NAME; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.TIMEOUT_MS; +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.Context; +import android.support.v4.media.session.MediaSessionCompat; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import com.google.android.exoplayer2.session.vct.common.HandlerThreadTestRule; +import com.google.android.exoplayer2.session.vct.common.MainLooperTestRule; +import com.google.android.exoplayer2.session.vct.common.TestHandler; +import java.util.concurrent.CountDownLatch; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link RemoteMediaControllerCompat}. */ +@RunWith(AndroidJUnit4.class) +public class RemoteMediaControllerCompatTest { + + @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); + + @Rule + public final HandlerThreadTestRule threadTestRule = + new HandlerThreadTestRule("RemoteMediaControllerCompatTest"); + + private TestHandler handler; + private MediaSessionCompat sessionCompat; + private RemoteMediaControllerCompat remoteControllerCompat; + + @Before + public void setUp() throws Exception { + Context context = ApplicationProvider.getApplicationContext(); + handler = threadTestRule.getHandler(); + handler.postAndSync( + () -> { + sessionCompat = new MediaSessionCompat(context, DEFAULT_TEST_NAME); + sessionCompat.setActive(true); + }); + remoteControllerCompat = + new RemoteMediaControllerCompat( + context, sessionCompat.getSessionToken(), /* waitForConnection= */ true); + } + + @After + public void cleanUp() { + sessionCompat.release(); + remoteControllerCompat.cleanUp(); + } + + @Test + @SmallTest + public void play() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + sessionCompat.setCallback( + new MediaSessionCompat.Callback() { + @Override + public void onPlay() { + latch.countDown(); + } + }, + handler); + + remoteControllerCompat.getTransportControls().play(); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/RemoteMediaSessionCompatTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/RemoteMediaSessionCompatTest.java new file mode 100644 index 0000000000..209cb041be --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/RemoteMediaSessionCompatTest.java @@ -0,0 +1,67 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.DEFAULT_TEST_NAME; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.MediaSessionCompat; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link RemoteMediaSessionCompat}. */ +@RunWith(AndroidJUnit4.class) +public class RemoteMediaSessionCompatTest { + + private Context context; + private RemoteMediaSessionCompat remoteSessionCompat; + + @Before + public void setUp() throws Exception { + context = ApplicationProvider.getApplicationContext(); + remoteSessionCompat = new RemoteMediaSessionCompat(DEFAULT_TEST_NAME, context); + } + + @After + public void cleanUp() throws Exception { + remoteSessionCompat.cleanUp(); + } + + @Test + @SmallTest + public void gettingToken() throws Exception { + MediaSessionCompat.Token token = remoteSessionCompat.getSessionToken(); + assertThat(token).isNotNull(); + } + + @Test + @SmallTest + public void creatingControllerCompat() throws Exception { + MediaSessionCompat.Token token = remoteSessionCompat.getSessionToken(); + assertThat(token).isNotNull(); + MediaControllerCompat controller = new MediaControllerCompat(context, token); + assertThat(controller.getPackageName()).isEqualTo(SUPPORT_APP_PACKAGE_NAME); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/RemoteMediaSessionTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/RemoteMediaSessionTest.java new file mode 100644 index 0000000000..6a7dfa1d5c --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/RemoteMediaSessionTest.java @@ -0,0 +1,77 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.DEFAULT_TEST_NAME; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.os.Bundle; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import com.google.android.exoplayer2.session.vct.common.TestUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link RemoteMediaSession}. */ +@RunWith(AndroidJUnit4.class) +public class RemoteMediaSessionTest { + + private Context context; + private RemoteMediaSession remoteSession; + private Bundle tokenExtras; + + @Before + public void setUp() throws Exception { + context = ApplicationProvider.getApplicationContext(); + tokenExtras = TestUtils.createTestBundle(); + remoteSession = new RemoteMediaSession(DEFAULT_TEST_NAME, context, tokenExtras); + } + + @After + public void cleanUp() throws Exception { + if (remoteSession != null) { + remoteSession.cleanUp(); + } + } + + @Test + @SmallTest + public void gettingToken() throws Exception { + SessionToken token = remoteSession.getToken(); + assertThat(token).isNotNull(); + assertThat(token.getPackageName()).isEqualTo(SUPPORT_APP_PACKAGE_NAME); + assertThat(TestUtils.equals(tokenExtras, token.getExtras())).isTrue(); + } + + @Test + @SmallTest + public void creatingController() throws Exception { + SessionToken token = remoteSession.getToken(); + assertThat(token).isNotNull(); + MediaController controller = + new MediaController.Builder(context) + .setSessionToken(token) + .setControllerCallback(new MediaController.ControllerCallback() {}) + .build(); + assertThat(controller).isNotNull(); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/SessionCommandTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/SessionCommandTest.java new file mode 100644 index 0000000000..513bd3b078 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/SessionCommandTest.java @@ -0,0 +1,146 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link SessionCommand} and {@link SessionCommands}. */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class SessionCommandTest { + // Prefix for all command codes + private static final String PREFIX_COMMAND_CODE = "COMMAND_CODE_"; + + private static final List PREFIX_COMMAND_CODES = new ArrayList<>(); + + static { + PREFIX_COMMAND_CODES.add("COMMAND_CODE_PLAYER_"); + PREFIX_COMMAND_CODES.add("COMMAND_CODE_VOLUME_"); + PREFIX_COMMAND_CODES.add("COMMAND_CODE_SESSION_"); + PREFIX_COMMAND_CODES.add("COMMAND_CODE_LIBRARY_"); + } + + /** Test possible typos in naming */ + @Test + public void codes_name() { + List fields = getSessionCommandsFields(""); + for (int i = 0; i < fields.size(); i++) { + String name = fields.get(i).getName(); + + boolean matches = false; + if (name.startsWith("COMMAND_VERSION_") || name.equals("COMMAND_CODE_CUSTOM")) { + matches = true; + } + if (!matches) { + for (int j = 0; j < PREFIX_COMMAND_CODES.size(); j++) { + if (name.startsWith(PREFIX_COMMAND_CODES.get(j))) { + matches = true; + break; + } + } + } + assertWithMessage("Unexpected constant " + name).that(matches).isTrue(); + } + } + + /** Tests possible code duplications in values */ + @Test + public void codes_valueDuplication() throws IllegalAccessException { + List fields = getSessionCommandsFields(PREFIX_COMMAND_CODE); + Set values = new HashSet<>(); + for (int i = 0; i < fields.size(); i++) { + Integer value = fields.get(i).getInt(null); + assertThat(values.add(value)).isTrue(); + } + } + + /** Tests whether codes are continuous */ + @Test + @Ignore + public void codes_valueContinuous() throws IllegalAccessException { + for (int i = 0; i < PREFIX_COMMAND_CODES.size(); i++) { + List fields = getSessionCommandsFields(PREFIX_COMMAND_CODES.get(i)); + List values = new ArrayList<>(); + for (int j = 0; j < fields.size(); j++) { + values.add(fields.get(j).getInt(null)); + } + Collections.sort(values); + for (int j = 1; j < values.size(); j++) { + assertWithMessage( + "Command code isn't continuous. Missing " + + (values.get(j - 1) + 1) + + " in " + + PREFIX_COMMAND_CODES.get(i)) + .that((int) values.get(j)) + .isEqualTo(((int) values.get(j - 1)) + 1); + } + } + } + + @Test + public void addAllPredefinedCommands_withVersion1_notHaveVersion2Commands() { + SessionCommands commands = + new SessionCommands.Builder() + .addAllPredefinedCommands(SessionCommand.COMMAND_VERSION_1) + .build(); + assertThat(commands.contains(SessionCommand.COMMAND_CODE_SESSION_SET_MEDIA_URI)).isFalse(); + } + + @Test + public void addAllPredefinedCommands_withVersion2_hasVersion2Commands() { + SessionCommands commands = + new SessionCommands.Builder() + .addAllPredefinedCommands(SessionCommand.COMMAND_VERSION_2) + .build(); + assertThat(commands.contains(SessionCommand.COMMAND_CODE_SESSION_SET_MEDIA_URI)).isTrue(); + } + + private static List getSessionCommandsFields(String prefix) { + List list = new ArrayList<>(); + Field[] fields = SessionCommand.class.getFields(); + if (fields != null) { + for (int i = 0; i < fields.length; i++) { + if (isPublicStaticFinalInt(fields[i]) && fields[i].getName().startsWith(prefix)) { + list.add(fields[i]); + } + } + } + return list; + } + + private static boolean isPublicStaticFinalInt(Field field) { + if (field.getType() != int.class) { + return false; + } + int modifier = field.getModifiers(); + return Modifier.isPublic(modifier) && Modifier.isStatic(modifier) && Modifier.isFinal(modifier); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/SessionTokenTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/SessionTokenTest.java new file mode 100644 index 0000000000..7b1912bf6e --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/SessionTokenTest.java @@ -0,0 +1,104 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.ComponentName; +import android.content.Context; +import android.os.Bundle; +import android.os.Process; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import com.google.android.exoplayer2.session.vct.common.MainLooperTestRule; +import com.google.android.exoplayer2.session.vct.common.TestUtils; +import java.util.ArrayList; +import java.util.List; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link SessionToken}. */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class SessionTokenTest { + + @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); + + private Context context; + private List sessions = new ArrayList<>(); + + @Before + public void setUp() throws Exception { + context = ApplicationProvider.getApplicationContext(); + } + + @After + public void cleanUp() throws Exception { + for (MediaSession session : sessions) { + if (session != null) { + session.release(); + } + } + } + + @Test + public void constructor_sessionService() { + SessionToken token = + new SessionToken( + context, + new ComponentName( + context.getPackageName(), MockMediaSessionService.class.getCanonicalName())); + assertThat(token.getPackageName()).isEqualTo(context.getPackageName()); + assertThat(token.getUid()).isEqualTo(Process.myUid()); + assertThat(token.getType()).isEqualTo(SessionToken.TYPE_SESSION_SERVICE); + } + + @Test + public void constructor_libraryService() { + ComponentName testComponentName = + new ComponentName( + context.getPackageName(), MockMediaLibraryService.class.getCanonicalName()); + SessionToken token = new SessionToken(context, testComponentName); + + assertThat(token.getPackageName()).isEqualTo(context.getPackageName()); + assertThat(token.getUid()).isEqualTo(Process.myUid()); + assertThat(token.getType()).isEqualTo(SessionToken.TYPE_LIBRARY_SERVICE); + assertThat(token.getServiceName()).isEqualTo(testComponentName.getClassName()); + } + + @Test + public void getters_whenCreatedBySession() { + Bundle testTokenExtras = TestUtils.createTestBundle(); + MediaSession session = + new MediaSession.Builder(context, new MockPlayer.Builder().build()) + .setId("testGetters_whenCreatedBySession") + .setExtras(testTokenExtras) + .build(); + sessions.add(session); + SessionToken token = session.getToken(); + + assertThat(token.getPackageName()).isEqualTo(context.getPackageName()); + assertThat(token.getUid()).isEqualTo(Process.myUid()); + assertThat(token.getType()).isEqualTo(SessionToken.TYPE_SESSION); + assertThat(TestUtils.equals(testTokenExtras, token.getExtras())).isTrue(); + assertThat(token.getServiceName()).isNull(); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/TestBrowserCallbackTest.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/TestBrowserCallbackTest.java new file mode 100644 index 0000000000..46c971efe5 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/androidTest/java/com/google/android/exoplayer2/session/TestBrowserCallbackTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.session; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import java.lang.reflect.Method; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link TestBrowserCallback}. */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class TestBrowserCallbackTest { + + /** + * Test if the {@link TestBrowserCallback} wraps the callback proxy without missing any method. + */ + @Test + public void methods_overridden() { + Method[] methods = TestBrowserCallback.class.getMethods(); + assertThat(methods).isNotNull(); + for (Method method : methods) { + // For any methods in the controller callback, TestBrowserCallback should have + // overridden the method and call matching API in the callback proxy. + assertWithMessage( + "TestBrowserCallback should override " + method + " and call callback proxy") + .that(method.getDeclaringClass()) + .isNotEqualTo(MediaBrowser.BrowserCallback.class); + assertWithMessage( + "TestBrowserCallback should override " + method + " and call callback proxy") + .that(method.getDeclaringClass()) + .isNotEqualTo(MediaController.ControllerCallback.class); + } + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/AndroidManifest.xml b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..3c11436c8c --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/AndroidManifest.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/LocalMockMediaBrowserServiceCompat.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/LocalMockMediaBrowserServiceCompat.java new file mode 100644 index 0000000000..ac8fdf0972 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/LocalMockMediaBrowserServiceCompat.java @@ -0,0 +1,19 @@ +/* + * Copyright 2021 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.session; + +/** {@link MockMediaBrowserServiceCompat} running on a local process. */ +public class LocalMockMediaBrowserServiceCompat extends MockMediaBrowserServiceCompat {} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/LocalMockMediaSessionService.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/LocalMockMediaSessionService.java new file mode 100644 index 0000000000..0c0206cb60 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/LocalMockMediaSessionService.java @@ -0,0 +1,19 @@ +/* + * Copyright 2021 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.session; + +/** {@link MockMediaSessionService} running on a local process. */ +public class LocalMockMediaSessionService extends MockMediaSessionService {} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MediaBrowserCompatProviderService.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MediaBrowserCompatProviderService.java new file mode 100644 index 0000000000..d800f343d1 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MediaBrowserCompatProviderService.java @@ -0,0 +1,200 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.ACTION_MEDIA_BROWSER_COMPAT; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_SESSION_COMPAT_TOKEN; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.SERVICE_CONNECTION_TIMEOUT_MS; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.app.Service; +import android.content.ComponentName; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.support.v4.media.MediaBrowserCompat; +import android.support.v4.media.MediaBrowserCompat.ConnectionCallback; +import android.support.v4.media.MediaBrowserCompat.ItemCallback; +import android.support.v4.media.MediaBrowserCompat.SearchCallback; +import android.support.v4.media.MediaBrowserCompat.SubscriptionCallback; +import com.google.android.exoplayer2.session.vct.common.IRemoteMediaBrowserCompat; +import com.google.android.exoplayer2.session.vct.common.TestHandler; +import com.google.android.exoplayer2.util.Log; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; + +/** + * A Service that creates {@link MediaBrowserCompat} and calls its methods according to the service + * app's requests. + */ +public class MediaBrowserCompatProviderService extends Service { + private static final String TAG = "MediaBrowserCompatProviderService"; + + Map mediaBrowserCompatMap = new HashMap<>(); + Map connectionCallbackMap = new HashMap<>(); + RemoteMediaBrowserCompatStub binder; + + TestHandler handler; + Executor executor; + + @Override + public void onCreate() { + super.onCreate(); + binder = new RemoteMediaBrowserCompatStub(); + + handler = new TestHandler(getMainLooper()); + executor = handler::post; + } + + @Override + public IBinder onBind(Intent intent) { + if (ACTION_MEDIA_BROWSER_COMPAT.equals(intent.getAction())) { + return binder; + } + return null; + } + + private class RemoteMediaBrowserCompatStub extends IRemoteMediaBrowserCompat.Stub { + @Override + public void create(String browserId, ComponentName componentName) throws RemoteException { + try { + TestBrowserConnectionCallback callback = new TestBrowserConnectionCallback(); + handler.postAndSync( + () -> { + MediaBrowserCompat browser = + new MediaBrowserCompat( + MediaBrowserCompatProviderService.this, + componentName, + callback, + new Bundle(/* rootHints= */ )); + + mediaBrowserCompatMap.put(browserId, browser); + connectionCallbackMap.put(browserId, callback); + }); + } catch (Exception e) { + Log.e(TAG, "Exception occurred while creating MediaMediaBrowserCompat", e); + } + } + + //////////////////////////////////////////////////////////////////////////////// + // MediaBrowserCompat methods + //////////////////////////////////////////////////////////////////////////////// + + @Override + public void connect(String browserId, boolean waitForConnection) throws RemoteException { + MediaBrowserCompat browser = mediaBrowserCompatMap.get(browserId); + browser.connect(); + + if (waitForConnection) { + TestBrowserConnectionCallback callback = connectionCallbackMap.get(browserId); + + boolean connected = false; + try { + connected = callback.connectionLatch.await(SERVICE_CONNECTION_TIMEOUT_MS, MILLISECONDS); + } catch (InterruptedException e) { + Log.e(TAG, "InterruptedException occurred while waiting for connection", e); + } + + if (!connected) { + Log.e(TAG, "Could not connect to the given browser service."); + } + } + } + + @Override + public void disconnect(String browserId) throws RemoteException { + MediaBrowserCompat browser = mediaBrowserCompatMap.get(browserId); + browser.disconnect(); + } + + @Override + public boolean isConnected(String browserId) throws RemoteException { + MediaBrowserCompat browser = mediaBrowserCompatMap.get(browserId); + return browser.isConnected(); + } + + @Override + public ComponentName getServiceComponent(String browserId) throws RemoteException { + MediaBrowserCompat browser = mediaBrowserCompatMap.get(browserId); + return browser.getServiceComponent(); + } + + @Override + public String getRoot(String browserId) throws RemoteException { + MediaBrowserCompat browser = mediaBrowserCompatMap.get(browserId); + return browser.getRoot(); + } + + @Override + public Bundle getExtras(String browserId) throws RemoteException { + MediaBrowserCompat browser = mediaBrowserCompatMap.get(browserId); + return browser.getExtras(); + } + + @Override + public Bundle getConnectedSessionToken(String browserId) throws RemoteException { + MediaBrowserCompat browser = mediaBrowserCompatMap.get(browserId); + Bundle tokenBundle = new Bundle(); + tokenBundle.putParcelable(KEY_SESSION_COMPAT_TOKEN, browser.getSessionToken()); + return tokenBundle; + } + + @Override + public void subscribe(String browserId, String parentId, Bundle options) + throws RemoteException { + MediaBrowserCompat browser = mediaBrowserCompatMap.get(browserId); + browser.subscribe(parentId, options, new SubscriptionCallback() {}); + } + + @Override + public void unsubscribe(String browserId, String parentId) throws RemoteException { + MediaBrowserCompat browser = mediaBrowserCompatMap.get(browserId); + browser.unsubscribe(parentId); + } + + @Override + public void getItem(String browserId, String mediaId) throws RemoteException { + MediaBrowserCompat browser = mediaBrowserCompatMap.get(browserId); + browser.getItem(mediaId, new ItemCallback() {}); + } + + @Override + public void search(String browserId, String query, Bundle extras) throws RemoteException { + MediaBrowserCompat browser = mediaBrowserCompatMap.get(browserId); + browser.search(query, extras, new SearchCallback() {}); + } + + @Override + public void sendCustomAction(String browserId, String action, Bundle extras) + throws RemoteException { + MediaBrowserCompat browser = mediaBrowserCompatMap.get(browserId); + browser.sendCustomAction(action, extras, /* customActionCallback= */ null); + } + } + + private class TestBrowserConnectionCallback extends ConnectionCallback { + private CountDownLatch connectionLatch = new CountDownLatch(1); + + @Override + public void onConnected() { + connectionLatch.countDown(); + } + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MediaControllerCompatProviderService.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MediaControllerCompatProviderService.java new file mode 100644 index 0000000000..9bba122c11 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MediaControllerCompatProviderService.java @@ -0,0 +1,312 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.ACTION_MEDIA_CONTROLLER_COMPAT; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_ARGUMENTS; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.SERVICE_CONNECTION_TIMEOUT_MS; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.app.Service; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Parcelable; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.support.v4.media.MediaDescriptionCompat; +import android.support.v4.media.RatingCompat; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import com.google.android.exoplayer2.session.vct.common.IRemoteMediaControllerCompat; +import com.google.android.exoplayer2.session.vct.common.TestHandler; +import com.google.android.exoplayer2.util.Log; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; + +/** + * A Service that creates {@link MediaControllerCompat} and calls its methods according to the + * service app's requests. + */ +public class MediaControllerCompatProviderService extends Service { + private static final String TAG = "MediaControllerCompatProviderService"; + + Map mediaControllerCompatMap = new HashMap<>(); + RemoteMediaControllerCompatStub binder; + + TestHandler handler; + Executor executor; + + @Override + public void onCreate() { + super.onCreate(); + binder = new RemoteMediaControllerCompatStub(); + + handler = new TestHandler(getMainLooper()); + executor = handler::post; + } + + @Override + public IBinder onBind(Intent intent) { + if (ACTION_MEDIA_CONTROLLER_COMPAT.equals(intent.getAction())) { + return binder; + } + return null; + } + + private class RemoteMediaControllerCompatStub extends IRemoteMediaControllerCompat.Stub { + + @Override + public void create(String controllerId, Bundle tokenBundle, boolean waitForConnection) { + MediaSessionCompat.Token token = (MediaSessionCompat.Token) getParcelable(tokenBundle); + MediaControllerCompat controller = + new MediaControllerCompat(MediaControllerCompatProviderService.this, token); + + TestControllerCallback callback = new TestControllerCallback(); + controller.registerCallback(callback, handler); + + mediaControllerCompatMap.put(controllerId, controller); + + if (!waitForConnection) { + return; + } + + boolean connected = false; + try { + connected = callback.connectionLatch.await(SERVICE_CONNECTION_TIMEOUT_MS, MILLISECONDS); + } catch (InterruptedException e) { + Log.e(TAG, "InterruptedException occurred while waiting for connection", e); + } + + if (!connected) { + Log.e(TAG, "Could not connect to the given session."); + } + } + + //////////////////////////////////////////////////////////////////////////////// + // MediaControllerCompat methods + //////////////////////////////////////////////////////////////////////////////// + + @Override + public void addQueueItem(String controllerId, Bundle descriptionBundle) throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + MediaDescriptionCompat desc = (MediaDescriptionCompat) getParcelable(descriptionBundle); + controller.addQueueItem(desc); + } + + @Override + public void addQueueItemWithIndex(String controllerId, Bundle descriptionBundle, int index) + throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + MediaDescriptionCompat desc = (MediaDescriptionCompat) getParcelable(descriptionBundle); + controller.addQueueItem(desc, index); + } + + @Override + public void removeQueueItem(String controllerId, Bundle descriptionBundle) + throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + MediaDescriptionCompat desc = (MediaDescriptionCompat) getParcelable(descriptionBundle); + controller.removeQueueItem(desc); + } + + @Override + public void setVolumeTo(String controllerId, int value, int flags) throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + controller.setVolumeTo(value, flags); + } + + @Override + public void adjustVolume(String controllerId, int direction, int flags) throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + controller.adjustVolume(direction, flags); + } + + @Override + public void sendCommand(String controllerId, String command, Bundle params, ResultReceiver cb) + throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + controller.sendCommand(command, params, cb); + } + + //////////////////////////////////////////////////////////////////////////////// + // MediaControllerCompat.TransportControls methods + //////////////////////////////////////////////////////////////////////////////// + + @Override + public void prepare(String controllerId) throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + controller.getTransportControls().prepare(); + } + + @Override + public void prepareFromMediaId(String controllerId, String mediaId, Bundle extras) + throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + controller.getTransportControls().prepareFromMediaId(mediaId, extras); + } + + @Override + public void prepareFromSearch(String controllerId, String query, Bundle extras) + throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + controller.getTransportControls().prepareFromSearch(query, extras); + } + + @Override + public void prepareFromUri(String controllerId, Uri uri, Bundle extras) throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + controller.getTransportControls().prepareFromUri(uri, extras); + } + + @Override + public void play(String controllerId) throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + controller.getTransportControls().play(); + } + + @Override + public void playFromMediaId(String controllerId, String mediaId, Bundle extras) + throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + controller.getTransportControls().playFromMediaId(mediaId, extras); + } + + @Override + public void playFromSearch(String controllerId, String query, Bundle extras) + throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + controller.getTransportControls().playFromSearch(query, extras); + } + + @Override + public void playFromUri(String controllerId, Uri uri, Bundle extras) throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + controller.getTransportControls().playFromUri(uri, extras); + } + + @Override + public void skipToQueueItem(String controllerId, long id) throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + controller.getTransportControls().skipToQueueItem(id); + } + + @Override + public void pause(String controllerId) throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + controller.getTransportControls().pause(); + } + + @Override + public void stop(String controllerId) throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + controller.getTransportControls().stop(); + } + + @Override + public void seekTo(String controllerId, long pos) throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + controller.getTransportControls().seekTo(pos); + } + + @Override + public void setPlaybackSpeed(String controllerId, float speed) throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + controller.getTransportControls().setPlaybackSpeed(speed); + } + + @Override + public void skipToNext(String controllerId) throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + controller.getTransportControls().skipToNext(); + } + + @Override + public void skipToPrevious(String controllerId) throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + controller.getTransportControls().skipToPrevious(); + } + + @Override + public void setRating(String controllerId, Bundle ratingBundle) throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + RatingCompat rating = (RatingCompat) getParcelable(ratingBundle); + controller.getTransportControls().setRating(rating); + } + + @Override + public void setRatingWithExtras(String controllerId, Bundle ratingBundle, Bundle extras) + throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + RatingCompat rating = (RatingCompat) getParcelable(ratingBundle); + controller.getTransportControls().setRating(rating, extras); + } + + @Override + public void setCaptioningEnabled(String controllerId, boolean enabled) throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + controller.getTransportControls().setCaptioningEnabled(enabled); + } + + @Override + public void setRepeatMode(String controllerId, int repeatMode) throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + controller.getTransportControls().setRepeatMode(repeatMode); + } + + @Override + public void setShuffleMode(String controllerId, int shuffleMode) throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + controller.getTransportControls().setShuffleMode(shuffleMode); + } + + @Override + public void sendCustomAction(String controllerId, Bundle customActionBundle, Bundle args) + throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + PlaybackStateCompat.CustomAction customAction = + (PlaybackStateCompat.CustomAction) getParcelable(customActionBundle); + controller.getTransportControls().sendCustomAction(customAction, args); + } + + @Override + public void sendCustomActionWithName(String controllerId, String action, Bundle args) + throws RemoteException { + MediaControllerCompat controller = mediaControllerCompatMap.get(controllerId); + controller.getTransportControls().sendCustomAction(action, args); + } + + private Parcelable getParcelable(Bundle bundle) { + bundle.setClassLoader(MediaSessionCompat.class.getClassLoader()); + return bundle.getParcelable(KEY_ARGUMENTS); + } + } + + private static class TestControllerCallback extends MediaControllerCompat.Callback { + private final CountDownLatch connectionLatch = new CountDownLatch(1); + + @Override + public void onSessionReady() { + super.onSessionReady(); + connectionLatch.countDown(); + } + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MediaControllerProviderService.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MediaControllerProviderService.java new file mode 100644 index 0000000000..d4a8d2cc34 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MediaControllerProviderService.java @@ -0,0 +1,630 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.ACTION_MEDIA2_CONTROLLER; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.SERVICE_CONNECTION_TIMEOUT_MS; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import androidx.annotation.NonNull; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaMetadata; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Rating; +import com.google.android.exoplayer2.session.vct.common.IRemoteMediaController; +import com.google.android.exoplayer2.session.vct.common.TestHandler; +import com.google.android.exoplayer2.session.vct.common.TestUtils; +import com.google.android.exoplayer2.util.Log; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeoutException; + +/** + * A Service that creates {@link MediaController} and calls its methods according to the service + * app's requests. + */ +public class MediaControllerProviderService extends Service { + private static final String TAG = "MediaControllerProviderService"; + + Map mediaControllerMap = new HashMap<>(); + RemoteMediaControllerStub binder; + + TestHandler handler; + + @Override + public void onCreate() { + super.onCreate(); + binder = new RemoteMediaControllerStub(); + + handler = new TestHandler(getMainLooper()); + } + + @Override + public IBinder onBind(Intent intent) { + if (ACTION_MEDIA2_CONTROLLER.equals(intent.getAction())) { + return binder; + } + return null; + } + + @Override + public void onDestroy() { + for (MediaController controller : mediaControllerMap.values()) { + try { + handler.postAndSync(controller::release); + } catch (Exception e) { + Log.e(TAG, "Exception in releasing controller", e); + } + } + } + + private class RemoteMediaControllerStub extends IRemoteMediaController.Stub { + + private void runOnHandler(@NonNull TestHandler.TestRunnable runnable) throws RemoteException { + try { + handler.postAndSync(runnable); + } catch (Exception e) { + Log.e(TAG, "Exception thrown while waiting for handler", e); + throw new RemoteException("Unexpected exception"); + } + } + + private V runOnHandler(@NonNull Callable callable) throws RemoteException { + try { + return handler.postAndSync(callable); + } catch (Exception e) { + Log.e(TAG, "Exception thrown while waiting for handler", e); + throw new RemoteException("Unexpected exception"); + } + } + + @Override + public void create( + boolean isBrowser, + String controllerId, + Bundle tokenBundle, + Bundle connectionHints, + boolean waitForConnection) + throws RemoteException { + SessionToken token = SessionToken.CREATOR.fromBundle(tokenBundle); + TestControllerCallback callback = new TestControllerCallback(); + + runOnHandler( + () -> { + Context context = MediaControllerProviderService.this; + MediaController controller; + if (isBrowser) { + MediaBrowser.Builder builder = + new MediaBrowser.Builder(context) + .setSessionToken(token) + .setControllerCallback(callback); + if (connectionHints != null) { + builder.setConnectionHints(connectionHints); + } + controller = builder.build(); + } else { + MediaController.Builder builder = + new MediaController.Builder(context) + .setSessionToken(token) + .setControllerCallback(callback); + if (connectionHints != null) { + builder.setConnectionHints(connectionHints); + } + controller = builder.build(); + } + mediaControllerMap.put(controllerId, controller); + }); + + if (!waitForConnection) { + return; + } + + boolean connected = false; + try { + connected = callback.connectionLatch.await(SERVICE_CONNECTION_TIMEOUT_MS, MILLISECONDS); + } catch (InterruptedException e) { + Log.e(TAG, "InterruptedException occurred while waiting for connection", e); + } + + if (!connected) { + Log.e(TAG, "Could not connect to the given session."); + } + } + + //////////////////////////////////////////////////////////////////////////////// + // MediaController methods + //////////////////////////////////////////////////////////////////////////////// + + @Override + public Bundle getConnectedSessionToken(String controllerId) throws RemoteException { + return runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + return BundleableUtils.toNullableBundle(controller.getConnectedToken()); + }); + } + + @Override + public void play(String controllerId) throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.play(); + }); + } + + @Override + public void pause(String controllerId) throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.pause(); + }); + } + + @Override + public void setPlayWhenReady(String controllerId, boolean playWhenReady) + throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.setPlayWhenReady(playWhenReady); + }); + } + + @Override + public void prepare(String controllerId) throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.prepare(); + }); + } + + @Override + public void seekToDefaultPosition(String controllerId) throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.seekToDefaultPosition(); + }); + } + + @Override + public void seekToDefaultPositionWithWindowIndex(String controllerId, int windowIndex) + throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.seekToDefaultPosition(windowIndex); + }); + } + + @Override + public void seekTo(String controllerId, long positionMs) throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.seekTo(positionMs); + }); + } + + @Override + public void seekToWithWindowIndex(String controllerId, int windowIndex, long positionMs) + throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.seekTo(windowIndex, positionMs); + }); + } + + @Override + public void setPlaybackParameters(String controllerId, Bundle playbackParametersBundle) + throws RemoteException { + PlaybackParameters playbackParameters = + PlaybackParameters.CREATOR.fromBundle(playbackParametersBundle); + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.setPlaybackParameters(playbackParameters); + }); + } + + @Override + public void setPlaybackSpeed(String controllerId, float speed) throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.setPlaybackSpeed(speed); + }); + } + + @Override + public void setMediaItems1( + String controllerId, List mediaItemBundles, boolean resetPosition) + throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.setMediaItems( + BundleableUtils.fromBundleList(MediaItem.CREATOR, mediaItemBundles), resetPosition); + }); + } + + @Override + public void setMediaItems2( + String controllerId, + List mediaItemBundles, + int startWindowIndex, + long startPositionMs) + throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.setMediaItems( + BundleableUtils.fromBundleList(MediaItem.CREATOR, mediaItemBundles), + startWindowIndex, + startPositionMs); + }); + } + + @Override + public void createAndSetFakeMediaItems(String controllerId, int size) throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + List itemList = new ArrayList<>(); + for (int i = 0; i < size; i++) { + // Make media ID of each item same with its index. + String mediaId = TestUtils.getMediaIdInFakeTimeline(i); + itemList.add(MediaTestUtils.createConvergedMediaItem(mediaId)); + } + controller.setMediaItems(itemList); + }); + } + + @Override + @SuppressWarnings("FutureReturnValueIgnored") + public void setMediaUri(String controllerId, Uri uri, Bundle extras) throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.setMediaUri(uri, extras); + }); + } + + @Override + public void setPlaylistMetadata(String controllerId, Bundle playlistMetadataBundle) + throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.setPlaylistMetadata( + MediaMetadata.CREATOR.fromBundle(playlistMetadataBundle)); + }); + } + + @Override + public void addMediaItems(String controllerId, int index, List mediaItemBundles) + throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.addMediaItems( + index, BundleableUtils.fromBundleList(MediaItem.CREATOR, mediaItemBundles)); + }); + } + + @Override + public void removeMediaItems(String controllerId, int fromIndex, int toIndex) + throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.removeMediaItems(fromIndex, toIndex); + }); + } + + @Override + public void moveMediaItems(String controllerId, int fromIndex, int toIndex, int newIndex) + throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.moveMediaItems(fromIndex, toIndex, newIndex); + }); + } + + @Override + public void previous(String controllerId) throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.previous(); + }); + } + + @Override + public void next(String controllerId) throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.next(); + }); + } + + @Override + public void setShuffleModeEnabled(String controllerId, boolean shuffleModeEnabled) + throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.setShuffleModeEnabled(shuffleModeEnabled); + }); + } + + @Override + public void setRepeatMode(String controllerId, int repeatMode) throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.setRepeatMode(repeatMode); + }); + } + + @Override + public void setVolumeTo(String controllerId, int value, int flags) throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.setDeviceVolume(value); + }); + } + + @Override + public void adjustVolume(String controllerId, int direction, int flags) throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + switch (direction) { + case AudioManager.ADJUST_RAISE: + controller.increaseDeviceVolume(); + break; + case AudioManager.ADJUST_LOWER: + controller.decreaseDeviceVolume(); + break; + case AudioManager.ADJUST_MUTE: + controller.setDeviceMuted(true); + break; + case AudioManager.ADJUST_UNMUTE: + controller.setDeviceMuted(false); + break; + case AudioManager.ADJUST_TOGGLE_MUTE: + controller.setDeviceMuted(controller.isDeviceMuted()); + break; + default: + throw new IllegalArgumentException("Unknown direction: " + direction); + } + }); + } + + @Override + public Bundle sendCustomCommand(String controllerId, Bundle command, Bundle args) + throws RemoteException { + MediaController controller = mediaControllerMap.get(controllerId); + Future future = + controller.sendCustomCommand(SessionCommand.CREATOR.fromBundle(command), args); + SessionResult result = getFutureResult(future); + return result.toBundle(); + } + + @Override + public Bundle setRating(String controllerId, String mediaId, Bundle rating) + throws RemoteException { + MediaController controller = mediaControllerMap.get(controllerId); + Future future = + controller.setRating(mediaId, Rating.CREATOR.fromBundle(rating)); + SessionResult result = getFutureResult(future); + return result.toBundle(); + } + + @Override + public void setVolume(String controllerId, float volume) throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.setVolume(volume); + }); + } + + @Override + public void setDeviceVolume(String controllerId, int volume) throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.setDeviceVolume(volume); + }); + } + + @Override + public void increaseDeviceVolume(String controllerId) throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.increaseDeviceVolume(); + }); + } + + @Override + public void decreaseDeviceVolume(String controllerId) throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.decreaseDeviceVolume(); + }); + } + + @Override + public void setDeviceMuted(String controllerId, boolean muted) throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.setDeviceMuted(muted); + }); + } + + @Override + public void release(String controllerId) throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.release(); + }); + } + + @Override + public void stop(String controllerId) throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.stop(); + }); + } + + //////////////////////////////////////////////////////////////////////////////// + // MediaBrowser methods + //////////////////////////////////////////////////////////////////////////////// + + @Override + public Bundle getLibraryRoot(String controllerId, Bundle libraryParams) throws RemoteException { + MediaBrowser browser = (MediaBrowser) mediaControllerMap.get(controllerId); + Future future = + browser.getLibraryRoot( + BundleableUtils.fromNullableBundle( + MediaLibraryService.LibraryParams.CREATOR, libraryParams)); + LibraryResult result = getFutureResult(future); + return result.toBundle(); + } + + @Override + public Bundle subscribe(String controllerId, String parentId, Bundle libraryParams) + throws RemoteException { + MediaBrowser browser = (MediaBrowser) mediaControllerMap.get(controllerId); + Future future = + browser.subscribe( + parentId, + BundleableUtils.fromNullableBundle( + MediaLibraryService.LibraryParams.CREATOR, libraryParams)); + LibraryResult result = getFutureResult(future); + return result.toBundle(); + } + + @Override + public Bundle unsubscribe(String controllerId, String parentId) throws RemoteException { + MediaBrowser browser = (MediaBrowser) mediaControllerMap.get(controllerId); + Future future = browser.unsubscribe(parentId); + LibraryResult result = getFutureResult(future); + return result.toBundle(); + } + + @Override + public Bundle getChildren( + String controllerId, String parentId, int page, int pageSize, Bundle libraryParams) + throws RemoteException { + MediaBrowser browser = (MediaBrowser) mediaControllerMap.get(controllerId); + Future future = + browser.getChildren( + parentId, + page, + pageSize, + BundleableUtils.fromNullableBundle( + MediaLibraryService.LibraryParams.CREATOR, libraryParams)); + LibraryResult result = getFutureResult(future); + return result.toBundle(); + } + + @Override + public Bundle getItem(String controllerId, String mediaId) throws RemoteException { + MediaBrowser browser = (MediaBrowser) mediaControllerMap.get(controllerId); + Future future = browser.getItem(mediaId); + LibraryResult result = getFutureResult(future); + return result.toBundle(); + } + + @Override + public Bundle search(String controllerId, String query, Bundle libraryParams) + throws RemoteException { + MediaBrowser browser = (MediaBrowser) mediaControllerMap.get(controllerId); + Future future = + browser.search( + query, + BundleableUtils.fromNullableBundle( + MediaLibraryService.LibraryParams.CREATOR, libraryParams)); + LibraryResult result = getFutureResult(future); + return result.toBundle(); + } + + @Override + public Bundle getSearchResult( + String controllerId, String query, int page, int pageSize, Bundle libraryParams) + throws RemoteException { + MediaBrowser browser = (MediaBrowser) mediaControllerMap.get(controllerId); + Future future = + browser.getSearchResult( + query, + page, + pageSize, + BundleableUtils.fromNullableBundle( + MediaLibraryService.LibraryParams.CREATOR, libraryParams)); + LibraryResult result = getFutureResult(future); + return result.toBundle(); + } + + private final class TestControllerCallback implements MediaBrowser.BrowserCallback { + + private final CountDownLatch connectionLatch = new CountDownLatch(1); + + @Override + public void onConnected(MediaController controller) { + connectionLatch.countDown(); + } + } + } + + private static T getFutureResult(Future future) throws RemoteException { + try { + return future.get(TestUtils.TIMEOUT_MS, MILLISECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new RemoteException("Exception thrown when getting result. " + e.getMessage()); + } + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MediaSessionCompatProviderService.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MediaSessionCompatProviderService.java new file mode 100644 index 0000000000..95dcef6014 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MediaSessionCompatProviderService.java @@ -0,0 +1,218 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.ACTION_MEDIA_SESSION_COMPAT; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_METADATA_COMPAT; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_PLAYBACK_STATE_COMPAT; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_QUEUE; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_SESSION_COMPAT_TOKEN; + +import android.app.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.MediaSessionCompat.QueueItem; +import android.support.v4.media.session.PlaybackStateCompat; +import androidx.annotation.Nullable; +import androidx.media.VolumeProviderCompat; +import com.google.android.exoplayer2.session.vct.common.IRemoteMediaSessionCompat; +import com.google.android.exoplayer2.session.vct.common.TestHandler; +import com.google.android.exoplayer2.util.Log; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; + +/** + * A Service that creates {@link MediaSessionCompat} and calls its methods according to the client + * app's requests. + */ +public class MediaSessionCompatProviderService extends Service { + private static final String TAG = "MediaSessionCompatProviderService"; + + Map sessionMap = new HashMap<>(); + RemoteMediaSessionCompatStub sessionBinder; + + TestHandler handler; + Executor executor; + + @Override + public void onCreate() { + super.onCreate(); + sessionBinder = new RemoteMediaSessionCompatStub(); + handler = new TestHandler(getMainLooper()); + executor = handler::post; + } + + @Override + public IBinder onBind(Intent intent) { + if (ACTION_MEDIA_SESSION_COMPAT.equals(intent.getAction())) { + return sessionBinder; + } + return null; + } + + @Override + public void onDestroy() { + for (MediaSessionCompat session : sessionMap.values()) { + session.release(); + } + } + + private class RemoteMediaSessionCompatStub extends IRemoteMediaSessionCompat.Stub { + @Override + public void create(String sessionTag) throws RemoteException { + try { + handler.postAndSync( + () -> { + MediaSessionCompat session = + new MediaSessionCompat(MediaSessionCompatProviderService.this, sessionTag); + sessionMap.put(sessionTag, session); + }); + } catch (Exception e) { + Log.e(TAG, "Exception occurred while creating MediaSessionCompat", e); + } + } + + //////////////////////////////////////////////////////////////////////////////// + // MediaSessionCompat methods + //////////////////////////////////////////////////////////////////////////////// + + @Override + public Bundle getSessionToken(String sessionTag) throws RemoteException { + MediaSessionCompat session = sessionMap.get(sessionTag); + Bundle result = new Bundle(); + result.putParcelable(KEY_SESSION_COMPAT_TOKEN, session.getSessionToken()); + return result; + } + + @Override + public void setPlaybackToLocal(String sessionTag, int stream) throws RemoteException { + MediaSessionCompat session = sessionMap.get(sessionTag); + session.setPlaybackToLocal(stream); + } + + @Override + public void setPlaybackToRemote( + String sessionTag, int volumeControl, int maxVolume, int currentVolume) + throws RemoteException { + MediaSessionCompat session = sessionMap.get(sessionTag); + session.setPlaybackToRemote( + new VolumeProviderCompat(volumeControl, maxVolume, currentVolume) { + @Override + public void onSetVolumeTo(int volume) { + setCurrentVolume(volume); + } + + @Override + public void onAdjustVolume(int direction) { + setCurrentVolume(getCurrentVolume() + direction); + } + }); + } + + @Override + public void release(String sessionTag) throws RemoteException { + MediaSessionCompat session = sessionMap.get(sessionTag); + session.release(); + } + + @Override + public void setPlaybackState(String sessionTag, Bundle stateBundle) throws RemoteException { + MediaSessionCompat session = sessionMap.get(sessionTag); + stateBundle.setClassLoader(MediaSessionCompat.class.getClassLoader()); + PlaybackStateCompat state = stateBundle.getParcelable(KEY_PLAYBACK_STATE_COMPAT); + session.setPlaybackState(state); + } + + @Override + public void setMetadata(String sessionTag, Bundle metadataBundle) throws RemoteException { + MediaSessionCompat session = sessionMap.get(sessionTag); + metadataBundle.setClassLoader(MediaSessionCompat.class.getClassLoader()); + MediaMetadataCompat metadata = metadataBundle.getParcelable(KEY_METADATA_COMPAT); + session.setMetadata(metadata); + } + + @Override + public void setQueue(String sessionTag, @Nullable Bundle queueBundle) throws RemoteException { + MediaSessionCompat session = sessionMap.get(sessionTag); + if (queueBundle == null) { + session.setQueue(null); + } else { + queueBundle.setClassLoader(MediaSessionCompat.class.getClassLoader()); + List queue = queueBundle.getParcelableArrayList(KEY_QUEUE); + session.setQueue(queue); + } + } + + @Override + public void setQueueTitle(String sessionTag, CharSequence title) throws RemoteException { + MediaSessionCompat session = sessionMap.get(sessionTag); + session.setQueueTitle(title); + } + + @Override + public void setRepeatMode(String sessionTag, @PlaybackStateCompat.RepeatMode int repeatMode) + throws RemoteException { + MediaSessionCompat session = sessionMap.get(sessionTag); + session.setRepeatMode(repeatMode); + } + + @Override + public void setShuffleMode(String sessionTag, @PlaybackStateCompat.ShuffleMode int shuffleMode) + throws RemoteException { + MediaSessionCompat session = sessionMap.get(sessionTag); + session.setShuffleMode(shuffleMode); + } + + @Override + public void setSessionActivity(String sessionTag, PendingIntent pi) throws RemoteException { + MediaSessionCompat session = sessionMap.get(sessionTag); + session.setSessionActivity(pi); + } + + @Override + public void setFlags(String sessionTag, int flags) throws RemoteException { + MediaSessionCompat session = sessionMap.get(sessionTag); + session.setFlags(flags); + } + + @Override + public void setRatingType(String sessionTag, int type) throws RemoteException { + MediaSessionCompat session = sessionMap.get(sessionTag); + session.setRatingType(type); + } + + @Override + public void sendSessionEvent(String sessionTag, String event, Bundle extras) + throws RemoteException { + MediaSessionCompat session = sessionMap.get(sessionTag); + session.sendSessionEvent(event, extras); + } + + @Override + public void setCaptioningEnabled(String sessionTag, boolean enabled) throws RemoteException { + MediaSessionCompat session = sessionMap.get(sessionTag); + session.setCaptioningEnabled(enabled); + } + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MediaSessionProviderService.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MediaSessionProviderService.java new file mode 100644 index 0000000000..2b729b58d4 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MediaSessionProviderService.java @@ -0,0 +1,822 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.ACTION_MEDIA2_SESSION; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_AUDIO_ATTRIBUTES; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_BUFFERED_PERCENTAGE; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_BUFFERED_POSITION; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_CONTENT_BUFFERED_POSITION; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_CONTENT_DURATION; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_CONTENT_POSITION; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_CURRENT_AD_GROUP_INDEX; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_CURRENT_AD_INDEX_IN_AD_GROUP; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_CURRENT_LIVE_OFFSET; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_CURRENT_PERIOD_INDEX; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_CURRENT_POSITION; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_CURRENT_WINDOW_INDEX; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_DEVICE_INFO; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_DEVICE_MUTED; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_DEVICE_VOLUME; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_DURATION; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_IS_LOADING; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_IS_PLAYING; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_IS_PLAYING_AD; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_MEDIA_ITEM; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_PLAYBACK_PARAMETERS; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_PLAYBACK_STATE; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_PLAYBACK_SUPPRESSION_REASON; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_PLAYER_ERROR; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_PLAYLIST_METADATA; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_PLAY_WHEN_READY; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_REPEAT_MODE; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_SHUFFLE_MODE_ENABLED; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_TIMELINE; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_TOTAL_BUFFERED_DURATION; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_VIDEO_SIZE; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_VOLUME; +import static com.google.android.exoplayer2.session.vct.common.MediaSessionConstants.TEST_CONTROLLER_CALLBACK_SESSION_REJECTS; +import static com.google.android.exoplayer2.session.vct.common.MediaSessionConstants.TEST_GET_SESSION_ACTIVITY; + +import android.app.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaMetadata; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.PositionInfo; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.audio.AudioAttributes; +import com.google.android.exoplayer2.device.DeviceInfo; +import com.google.android.exoplayer2.session.MediaSession.ControllerInfo; +import com.google.android.exoplayer2.session.vct.common.IRemoteMediaSession; +import com.google.android.exoplayer2.session.vct.common.MockActivity; +import com.google.android.exoplayer2.session.vct.common.TestHandler; +import com.google.android.exoplayer2.session.vct.common.TestHandler.TestRunnable; +import com.google.android.exoplayer2.session.vct.common.TestUtils; +import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.video.VideoSize; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; + +/** + * A Service that creates {@link MediaSession} and calls its methods according to the client app's + * requests. + */ +public class MediaSessionProviderService extends Service { + private static final String TAG = "MediaSessionProviderService"; + + private Map sessionMap = new HashMap<>(); + private RemoteMediaSessionStub sessionBinder; + + private TestHandler handler; + + @Override + public void onCreate() { + super.onCreate(); + sessionBinder = new RemoteMediaSessionStub(); + handler = new TestHandler(getMainLooper()); + } + + @Override + public IBinder onBind(Intent intent) { + if (ACTION_MEDIA2_SESSION.equals(intent.getAction())) { + return sessionBinder; + } + return null; + } + + @Override + public void onDestroy() { + for (MediaSession session : sessionMap.values()) { + session.release(); + } + } + + private class RemoteMediaSessionStub extends IRemoteMediaSession.Stub { + + private void runOnHandler(@NonNull TestRunnable runnable) throws RemoteException { + try { + handler.postAndSync(runnable); + } catch (Exception e) { + Log.e(TAG, "Exception thrown while waiting for handler", e); + throw new RemoteException("Unexpected exception"); + } + } + + private V runOnHandler(@NonNull Callable callable) throws RemoteException { + try { + return handler.postAndSync(callable); + } catch (Exception e) { + Log.e(TAG, "Exception thrown while waiting for handler", e); + throw new RemoteException("Unexpected exception"); + } + } + + @Override + public void create(String sessionId, Bundle tokenExtras) throws RemoteException { + MediaSession.Builder builder = + new MediaSession.Builder( + MediaSessionProviderService.this, + new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build()) + .setId(sessionId); + + if (tokenExtras != null) { + builder.setExtras(tokenExtras); + } + + switch (sessionId) { + case TEST_GET_SESSION_ACTIVITY: + { + Intent sessionActivity = + new Intent(MediaSessionProviderService.this, MockActivity.class); + PendingIntent pendingIntent = + PendingIntent.getActivity( + MediaSessionProviderService.this, + /* requestCode= */ 0, + sessionActivity, + /* flags= */ 0); + builder.setSessionActivity(pendingIntent); + break; + } + case TEST_CONTROLLER_CALLBACK_SESSION_REJECTS: + { + builder.setSessionCallback( + new MediaSession.SessionCallback() { + @Nullable + @Override + public MediaSession.ConnectResult onConnect( + MediaSession session, ControllerInfo controller) { + return null; + } + }); + break; + } + } + + runOnHandler( + () -> { + MediaSession session = builder.build(); + session.setSessionPositionUpdateDelayMs(0L); + sessionMap.put(sessionId, session); + }); + } + + //////////////////////////////////////////////////////////////////////////////// + // MediaSession methods + //////////////////////////////////////////////////////////////////////////////// + + @Override + public Bundle getToken(String sessionId) throws RemoteException { + return runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + return session.getToken().toBundle(); + }); + } + + @Override + public Bundle getCompatToken(String sessionId) throws RemoteException { + return runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + return session.getSessionCompat().getSessionToken().toBundle(); + }); + } + + @Override + public void setSessionPositionUpdateDelayMs(String sessionId, long updateDelayMs) + throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + session.setSessionPositionUpdateDelayMs(updateDelayMs); + }); + } + + @Override + public void setPlayer(String sessionId, @NonNull Bundle config) throws RemoteException { + runOnHandler( + () -> { + config.setClassLoader(MediaSession.class.getClassLoader()); + MediaSession session = sessionMap.get(sessionId); + session.setPlayer(createMockPlayer(config)); + }); + } + + private SessionPlayer createMockPlayer(Bundle config) { + MockPlayer player = new MockPlayer.Builder().build(); + player.playerError = + BundleableUtils.fromNullableBundle( + ExoPlaybackException.CREATOR, config.getBundle(KEY_PLAYER_ERROR), player.playerError); + player.currentPosition = config.getLong(KEY_CURRENT_POSITION, player.currentPosition); + player.bufferedPosition = config.getLong(KEY_BUFFERED_POSITION, player.bufferedPosition); + player.bufferedPercentage = config.getInt(KEY_BUFFERED_PERCENTAGE, player.bufferedPercentage); + player.duration = config.getLong(KEY_DURATION, player.duration); + player.totalBufferedDuration = + config.getLong(KEY_TOTAL_BUFFERED_DURATION, player.totalBufferedDuration); + player.currentLiveOffset = config.getLong(KEY_CURRENT_LIVE_OFFSET, player.currentLiveOffset); + player.contentDuration = config.getLong(KEY_CONTENT_DURATION, player.contentDuration); + player.contentPosition = config.getLong(KEY_CONTENT_POSITION, player.contentPosition); + player.contentBufferedPosition = + config.getLong(KEY_CONTENT_BUFFERED_POSITION, player.contentBufferedPosition); + player.isPlayingAd = config.getBoolean(KEY_IS_PLAYING_AD, player.isPlayingAd); + player.currentAdGroupIndex = + config.getInt(KEY_CURRENT_AD_GROUP_INDEX, player.currentAdGroupIndex); + player.currentAdIndexInAdGroup = + config.getInt(KEY_CURRENT_AD_INDEX_IN_AD_GROUP, player.currentAdIndexInAdGroup); + player.playbackParameters = + BundleableUtils.fromNullableBundle( + PlaybackParameters.CREATOR, + config.getBundle(KEY_PLAYBACK_PARAMETERS), + player.playbackParameters); + player.timeline = + BundleableUtils.fromNullableBundle( + Timeline.CREATOR, config.getBundle(KEY_TIMELINE), player.timeline); + player.currentWindowIndex = + config.getInt(KEY_CURRENT_WINDOW_INDEX, player.currentWindowIndex); + player.currentPeriodIndex = + config.getInt(KEY_CURRENT_PERIOD_INDEX, player.currentPeriodIndex); + player.currentMediaItem = + BundleableUtils.fromNullableBundle( + MediaItem.CREATOR, config.getBundle(KEY_MEDIA_ITEM), player.currentMediaItem); + player.playlistMetadata = + BundleableUtils.fromNullableBundle( + MediaMetadata.CREATOR, + config.getBundle(KEY_PLAYLIST_METADATA), + player.playlistMetadata); + player.videoSize = + BundleableUtils.fromNullableBundle( + VideoSize.CREATOR, config.getBundle(KEY_VIDEO_SIZE), player.videoSize); + player.volume = config.getFloat(KEY_VOLUME, player.volume); + player.audioAttributes = + BundleableUtils.fromNullableBundle( + AudioAttributes.CREATOR, + config.getBundle(KEY_AUDIO_ATTRIBUTES), + player.audioAttributes); + player.deviceInfo = + BundleableUtils.fromNullableBundle( + DeviceInfo.CREATOR, config.getBundle(KEY_DEVICE_INFO), player.deviceInfo); + player.deviceVolume = config.getInt(KEY_DEVICE_VOLUME, player.deviceVolume); + player.deviceMuted = config.getBoolean(KEY_DEVICE_MUTED, player.deviceMuted); + player.playWhenReady = config.getBoolean(KEY_PLAY_WHEN_READY, player.playWhenReady); + player.playbackSuppressionReason = + config.getInt(KEY_PLAYBACK_SUPPRESSION_REASON, player.playbackSuppressionReason); + player.playbackState = config.getInt(KEY_PLAYBACK_STATE, player.playbackState); + player.isPlaying = config.getBoolean(KEY_IS_PLAYING, player.isPlaying); + player.isLoading = config.getBoolean(KEY_IS_LOADING, player.isLoading); + player.repeatMode = config.getInt(KEY_REPEAT_MODE, player.repeatMode); + player.shuffleModeEnabled = + config.getBoolean(KEY_SHUFFLE_MODE_ENABLED, player.shuffleModeEnabled); + return player; + } + + @Override + public void broadcastCustomCommand(String sessionId, Bundle command, Bundle args) + throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + session.broadcastCustomCommand(SessionCommand.CREATOR.fromBundle(command), args); + }); + } + + @Override + @SuppressWarnings("FutureReturnValueIgnored") + public void sendCustomCommand(String sessionId, Bundle controller, Bundle command, Bundle args) + throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + ControllerInfo info = MediaTestUtils.getTestControllerInfo(session); + session.sendCustomCommand(info, SessionCommand.CREATOR.fromBundle(command), args); + }); + } + + @Override + public void release(String sessionId) throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + session.release(); + }); + } + + @Override + public void setAvailableCommands( + String sessionId, Bundle controller, Bundle sessionCommands, Bundle playerCommands) + throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + ControllerInfo info = MediaTestUtils.getTestControllerInfo(session); + session.setAvailableCommands( + info, + SessionCommands.CREATOR.fromBundle(sessionCommands), + Player.Commands.CREATOR.fromBundle(playerCommands)); + }); + } + + @Override + @SuppressWarnings("FutureReturnValueIgnored") + public void setCustomLayout(String sessionId, Bundle controller, List layout) + throws RemoteException { + if (layout == null) { + return; + } + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + ControllerInfo info = MediaTestUtils.getTestControllerInfo(session); + List buttons = new ArrayList<>(); + for (Bundle bundle : layout) { + buttons.add(CommandButton.CREATOR.fromBundle(bundle)); + } + session.setCustomLayout(info, buttons); + }); + } + + //////////////////////////////////////////////////////////////////////////////// + // MockPlayer methods + //////////////////////////////////////////////////////////////////////////////// + + @Override + public void notifyPlayerError(String sessionId, @Nullable Bundle playerErrorBundle) + throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + @Nullable + ExoPlaybackException playerError = + BundleableUtils.fromNullableBundle( + ExoPlaybackException.CREATOR, playerErrorBundle, player.playerError); + player.notifyPlayerError(playerError); + }); + } + + @Override + public void setPlayWhenReady(String sessionId, boolean playWhenReady, int reason) + throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.playWhenReady = playWhenReady; + player.playbackSuppressionReason = reason; + }); + } + + @Override + public void setPlaybackState(String sessionId, int state) throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.playbackState = state; + }); + } + + @Override + public void setCurrentPosition(String sessionId, long pos) throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.currentPosition = pos; + }); + } + + @Override + public void setBufferedPosition(String sessionId, long pos) throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.bufferedPosition = pos; + }); + } + + @Override + public void setDuration(String sessionId, long duration) throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.duration = duration; + }); + } + + @Override + public void setBufferedPercentage(String sessionId, int bufferedPercentage) + throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.bufferedPercentage = bufferedPercentage; + }); + } + + @Override + public void setTotalBufferedDuration(String sessionId, long totalBufferedDuration) + throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.totalBufferedDuration = totalBufferedDuration; + }); + } + + @Override + public void setCurrentLiveOffset(String sessionId, long currentLiveOffset) + throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.currentLiveOffset = currentLiveOffset; + }); + } + + @Override + public void setContentDuration(String sessionId, long contentDuration) throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.contentDuration = contentDuration; + }); + } + + @Override + public void setContentPosition(String sessionId, long contentPosition) throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.contentPosition = contentPosition; + }); + } + + @Override + public void setContentBufferedPosition(String sessionId, long contentBufferedPosition) + throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.contentBufferedPosition = contentBufferedPosition; + }); + } + + @Override + public void setPlaybackParameters(String sessionId, Bundle playbackParametersBundle) + throws RemoteException { + PlaybackParameters playbackParameters = + PlaybackParameters.CREATOR.fromBundle(playbackParametersBundle); + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.setPlaybackParameters(playbackParameters); + }); + } + + @Override + public void setIsPlayingAd(String sessionId, boolean isPlayingAd) throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.isPlayingAd = isPlayingAd; + }); + } + + @Override + public void setCurrentAdGroupIndex(String sessionId, int currentAdGroupIndex) + throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.currentAdGroupIndex = currentAdGroupIndex; + }); + } + + @Override + public void setCurrentAdIndexInAdGroup(String sessionId, int currentAdIndexInAdGroup) + throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.currentAdIndexInAdGroup = currentAdIndexInAdGroup; + }); + } + + @Override + public void notifyPlayWhenReadyChanged( + String sessionId, boolean playWhenReady, @Player.PlaybackSuppressionReason int reason) + throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.notifyPlayWhenReadyChanged(playWhenReady, reason); + }); + } + + @Override + public void notifyPlaybackStateChanged(String sessionId, @Player.State int state) + throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.notifyPlaybackStateChanged(state); + }); + } + + @Override + public void notifyIsPlayingChanged(String sessionId, boolean isPlaying) throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.notifyIsPlayingChanged(isPlaying); + }); + } + + @Override + public void notifyIsLoadingChanged(String sessionId, boolean isLoading) throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.notifyIsLoadingChanged(isLoading); + }); + } + + @Override + public void notifyPositionDiscontinuity( + String sessionId, + Bundle oldPositionBundle, + Bundle newPositionBundle, + @DiscontinuityReason int reason) + throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.notifyPositionDiscontinuity( + PositionInfo.CREATOR.fromBundle(oldPositionBundle), + PositionInfo.CREATOR.fromBundle(newPositionBundle), + reason); + }); + } + + @Override + public void notifyPlaybackParametersChanged(String sessionId, Bundle playbackParametersBundle) + throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.notifyPlaybackParametersChanged( + PlaybackParameters.CREATOR.fromBundle(playbackParametersBundle)); + }); + } + + @Override + public void notifyMediaItemTransition( + String sessionId, int index, @Player.MediaItemTransitionReason int reason) + throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + Timeline.Window window = new Timeline.Window(); + @Nullable + MediaItem mediaItem = + index == C.INDEX_UNSET ? null : player.timeline.getWindow(index, window).mediaItem; + player.currentMediaItem = mediaItem; + player.notifyMediaItemTransition(mediaItem, reason); + }); + } + + @Override + public void notifyAudioAttributesChanged( + @NonNull String sessionId, @NonNull Bundle audioAttributesBundle) throws RemoteException { + AudioAttributes audioAttributes = AudioAttributes.CREATOR.fromBundle(audioAttributesBundle); + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.audioAttributes = audioAttributes; + player.notifyAudioAttributesChanged(audioAttributes); + }); + } + + //////////////////////////////////////////////////////////////////////////////// + // MockPlaylistAgent methods + //////////////////////////////////////////////////////////////////////////////// + + @Override + public void setTimeline(String sessionId, Bundle timelineBundle) throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.timeline = Timeline.CREATOR.fromBundle(timelineBundle); + }); + } + + @Override + public void createAndSetFakeTimeline(String sessionId, int windowCount) throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + + List mediaItems = new ArrayList<>(); + for (int windowIndex = 0; windowIndex < windowCount; windowIndex++) { + mediaItems.add( + MediaTestUtils.createConvergedMediaItem( + TestUtils.getMediaIdInFakeTimeline(windowIndex))); + } + player.timeline = new PlaylistTimeline(mediaItems); + }); + } + + @Override + public void setPlaylistMetadata(String sessionId, Bundle playlistMetadataBundle) + throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.playlistMetadata = MediaMetadata.CREATOR.fromBundle(playlistMetadataBundle); + }); + } + + @Override + public void setShuffleModeEnabled(String sessionId, boolean shuffleModeEnabled) + throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.shuffleModeEnabled = shuffleModeEnabled; + }); + } + + @Override + public void setRepeatMode(String sessionId, int repeatMode) throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.repeatMode = repeatMode; + }); + } + + @Override + public void setCurrentWindowIndex(String sessionId, int index) throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.currentWindowIndex = index; + }); + } + + @Override + public void notifyAvailableCommandsChanged(String sessionId, Bundle commandsBundle) + throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.notifyAvailableCommandsChanged( + BundleableUtils.fromNullableBundle( + Player.Commands.CREATOR, commandsBundle, Player.Commands.EMPTY)); + }); + } + + @Override + public void notifyTimelineChanged(String sessionId, @Player.TimelineChangeReason int reason) + throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.notifyTimelineChanged(reason); + }); + } + + @Override + public void notifyPlaylistMetadataChanged(String sessionId) throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.notifyPlaylistMetadataChanged(); + }); + } + + @Override + public void notifyShuffleModeEnabledChanged(String sessionId) throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.notifyShuffleModeEnabledChanged(); + }); + } + + @Override + public void notifyRepeatModeChanged(String sessionId) throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.notifyRepeatModeChanged(); + }); + } + + @Override + public void notifyVideoSizeChanged(String sessionId, Bundle videoSize) throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + VideoSize videoSizeObj = VideoSize.CREATOR.fromBundle(videoSize); + player.notifyVideoSizeChanged(videoSizeObj); + }); + } + + @Override + public boolean surfaceExists(String sessionId) throws RemoteException { + return runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + return player.surfaceExists(); + }); + } + + @Override + public void notifyDeviceVolumeChanged(String sessionId, int volume, boolean muted) + throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.deviceVolume = volume; + player.deviceMuted = muted; + player.notifyDeviceVolumeChanged(); + }); + } + + @Override + public void notifyDeviceInfoChanged(String sessionId, @NonNull Bundle deviceInfoBundle) + throws RemoteException { + DeviceInfo deviceInfo = DeviceInfo.CREATOR.fromBundle(deviceInfoBundle); + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.deviceInfo = deviceInfo; + player.notifyDeviceInfoChanged(); + }); + } + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MediaTestUtils.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MediaTestUtils.java new file mode 100644 index 0000000000..b59bd97287 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MediaTestUtils.java @@ -0,0 +1,160 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; +import static com.google.common.truth.Truth.assertThat; + +import android.os.Bundle; +import android.support.v4.media.MediaBrowserCompat; +import android.support.v4.media.MediaDescriptionCompat; +import android.support.v4.media.session.MediaSessionCompat; +import androidx.annotation.Nullable; +import androidx.media.MediaBrowserServiceCompat.BrowserRoot; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.session.MediaLibraryService.LibraryParams; +import com.google.android.exoplayer2.session.MediaSession.ControllerInfo; +import com.google.android.exoplayer2.session.vct.common.TestUtils; +import com.google.android.exoplayer2.util.Log; +import java.util.ArrayList; +import java.util.List; + +/** Utilities for tests. */ +public final class MediaTestUtils { + + private static final String TAG = "MediaTestUtils"; + + /** Create a media item with the mediaId for testing purpose. */ + public static MediaItem createConvergedMediaItem(String mediaId) { + return new MediaItem.Builder().setMediaId(mediaId).build(); + } + + public static List createConvergedMediaItems(int size) { + List list = new ArrayList<>(); + for (int i = 0; i < size; i++) { + list.add(createConvergedMediaItem("mediaItem_" + (i + 1))); + } + return list; + } + + public static ControllerInfo getTestControllerInfo(MediaSession session) { + if (session == null) { + return null; + } + for (ControllerInfo info : session.getConnectedControllers()) { + if (SUPPORT_APP_PACKAGE_NAME.equals(info.getPackageName())) { + return info; + } + } + Log.e(TAG, "Test controller was not found in connected controllers. session=" + session); + return null; + } + + /** + * Create a list of {@link MediaBrowserCompat.MediaItem} for testing purpose. + * + * @param size list size + * @return the newly created playlist + */ + public static List createBrowserItems(int size) { + List list = new ArrayList<>(); + for (int i = 0; i < size; i++) { + list.add( + new MediaBrowserCompat.MediaItem( + new MediaDescriptionCompat.Builder().setMediaId("browserItem_" + (i + 1)).build(), + /* flags= */ 0)); + } + return list; + } + + /** + * Create a list of {@link MediaSessionCompat.QueueItem} for testing purpose. + * + * @param size list size + * @return the newly created playlist + */ + public static List createQueueItems(int size) { + List list = new ArrayList<>(); + for (int i = 0; i < size; i++) { + list.add( + new MediaSessionCompat.QueueItem( + new MediaDescriptionCompat.Builder().setMediaId("queueItem_" + (i + 1)).build(), i)); + } + return list; + } + + public static Timeline createTimeline(int windowCount) { + return new PlaylistTimeline(createConvergedMediaItems(/* size= */ windowCount)); + } + + public static LibraryParams createLibraryParams() { + Bundle extras = new Bundle(); + extras.putString("key", "value"); + return new LibraryParams.Builder().setExtras(extras).build(); + } + + public static void assertLibraryParamsEquals( + @Nullable LibraryParams a, @Nullable LibraryParams b) { + if (a == null || b == null) { + assertThat(b).isEqualTo(a); + } else { + assertThat(b.recent).isEqualTo(a.recent); + assertThat(b.offline).isEqualTo(a.offline); + assertThat(b.suggested).isEqualTo(a.suggested); + assertThat(TestUtils.equals(a.extras, b.extras)).isTrue(); + } + } + + public static void assertLibraryParamsEquals( + @Nullable LibraryParams params, @Nullable Bundle rootExtras) { + if (params == null || rootExtras == null) { + assertThat(params).isNull(); + assertThat(rootExtras).isNull(); + } else { + assertThat(rootExtras.getBoolean(BrowserRoot.EXTRA_RECENT)).isEqualTo(params.recent); + assertThat(rootExtras.getBoolean(BrowserRoot.EXTRA_OFFLINE)).isEqualTo(params.offline); + assertThat(rootExtras.getBoolean(BrowserRoot.EXTRA_SUGGESTED)).isEqualTo(params.suggested); + assertThat(TestUtils.contains(rootExtras, params.extras)).isTrue(); + } + } + + public static void assertPaginatedListHasIds( + List paginatedList, List fullIdList, int page, int pageSize) { + int fromIndex = page * pageSize; + int toIndex = Math.min((page + 1) * pageSize, fullIdList.size()); + // Compare the given results with originals. + for (int originalIndex = fromIndex; originalIndex < toIndex; originalIndex++) { + int relativeIndex = originalIndex - fromIndex; + assertThat(paginatedList.get(relativeIndex).mediaId).isEqualTo(fullIdList.get(originalIndex)); + } + } + + public static void assertMediaIdEquals(MediaItem expected, MediaItem actual) { + assertThat(actual.mediaId).isEqualTo(expected.mediaId); + } + + public static void assertMediaIdEquals(Timeline expected, Timeline actual) { + assertThat(actual.getWindowCount()).isEqualTo(expected.getWindowCount()); + Timeline.Window expectedWindow = new Timeline.Window(); + Timeline.Window actualWindow = new Timeline.Window(); + for (int i = 0; i < expected.getWindowCount(); i++) { + assertMediaIdEquals( + expected.getWindow(i, expectedWindow).mediaItem, + actual.getWindow(i, actualWindow).mediaItem); + } + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MockMediaBrowserServiceCompat.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MockMediaBrowserServiceCompat.java new file mode 100644 index 0000000000..2a0d828ea0 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MockMediaBrowserServiceCompat.java @@ -0,0 +1,259 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserServiceCompatConstants.TEST_CONNECT_REJECTED; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserServiceCompatConstants.TEST_ON_CHILDREN_CHANGED_SUBSCRIBE_AND_UNSUBSCRIBE; + +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.support.v4.media.MediaBrowserCompat.MediaItem; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.MediaSessionCompat.Callback; +import androidx.annotation.GuardedBy; +import androidx.media.MediaBrowserServiceCompat; +import com.google.android.exoplayer2.session.vct.common.IRemoteMediaBrowserServiceCompat; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; + +/** Mock implementation of the media browser service. */ +public class MockMediaBrowserServiceCompat extends MediaBrowserServiceCompat { + private static final String TAG = "MockMediaBrowserServiceCompat"; + private static final Object lock = new Object(); + + @GuardedBy("lock") + private static volatile MockMediaBrowserServiceCompat instance; + + @GuardedBy("lock") + private static volatile Proxy serviceProxy; + + private MediaSessionCompat sessionCompat; + + private RemoteMediaBrowserServiceCompatStub testBinder; + + @Override + public void onCreate() { + super.onCreate(); + synchronized (lock) { + instance = this; + } + sessionCompat = new MediaSessionCompat(this, TAG); + sessionCompat.setCallback(new Callback() {}); + sessionCompat.setActive(true); + setSessionToken(sessionCompat.getSessionToken()); + + testBinder = new RemoteMediaBrowserServiceCompatStub(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + sessionCompat.release(); + synchronized (lock) { + instance = null; + // Note: Don't reset serviceProxy. + // When a test is finished and its next test is running, this service will be + // destroyed and re-created for the next test. When it happens, onDestroy() may be + // called after the next test's proxy has set because onDestroy() and tests run on + // the different threads. + // So keep serviceProxy for the next test. + } + } + + @Override + public IBinder onBind(Intent intent) { + String action = intent.getAction(); + if (SERVICE_INTERFACE.equals(action)) { + // for MediaBrowser + return super.onBind(intent); + } + // for RemoteMediaBrowserServiceCompat + return testBinder; + } + + public static MockMediaBrowserServiceCompat getInstance() { + synchronized (lock) { + return instance; + } + } + + public static void setMediaBrowserServiceProxy(Proxy proxy) { + synchronized (lock) { + serviceProxy = proxy; + } + } + + private static boolean isProxyOverridesMethod(String methodName) { + return isProxyOverridesMethod(methodName, -1); + } + + private static boolean isProxyOverridesMethod(String methodName, int paramCount) { + synchronized (lock) { + if (serviceProxy == null) { + return false; + } + Method[] methods = serviceProxy.getClass().getMethods(); + if (methods == null) { + return false; + } + for (int i = 0; i < methods.length; i++) { + if (methods[i].getName().equals(methodName)) { + if (paramCount < 0 + || (methods[i].getParameterTypes() != null + && methods[i].getParameterTypes().length == paramCount)) { + // Found method. Check if it overrides + return methods[i].getDeclaringClass() != Proxy.class; + } + } + } + return false; + } + } + + @Override + public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { + if (!SUPPORT_APP_PACKAGE_NAME.equals(clientPackageName)) { + // Test only -- reject any other request. + return null; + } + synchronized (lock) { + if (isProxyOverridesMethod("onGetRoot")) { + return serviceProxy.onGetRoot(clientPackageName, clientUid, rootHints); + } + } + return new BrowserRoot("stub", null); + } + + @Override + public void onLoadChildren(String parentId, Result> result) { + synchronized (lock) { + if (isProxyOverridesMethod("onLoadChildren", 2)) { + serviceProxy.onLoadChildren(parentId, result); + return; + } + } + } + + @Override + public void onLoadChildren(String parentId, Result> result, Bundle options) { + synchronized (lock) { + if (isProxyOverridesMethod("onLoadChildren", 3)) { + serviceProxy.onLoadChildren(parentId, result, options); + return; + } + } + super.onLoadChildren(parentId, result, options); + } + + @Override + public void onLoadItem(String itemId, Result result) { + synchronized (lock) { + if (isProxyOverridesMethod("onLoadItem")) { + serviceProxy.onLoadItem(itemId, result); + return; + } + } + super.onLoadItem(itemId, result); + } + + @Override + public void onSearch(String query, Bundle extras, Result> result) { + synchronized (lock) { + if (isProxyOverridesMethod("onSearch")) { + serviceProxy.onSearch(query, extras, result); + return; + } + } + super.onSearch(query, extras, result); + } + + @Override + public void onCustomAction(String action, Bundle extras, Result result) { + synchronized (lock) { + if (isProxyOverridesMethod("onCustomAction")) { + serviceProxy.onCustomAction(action, extras, result); + return; + } + } + super.onCustomAction(action, extras, result); + } + + /** Proxy for MediaBrowserServiceCompat callbacks */ + public static class Proxy { + public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { + return new BrowserRoot("stub", null); + } + + public void onLoadChildren(String parentId, Result> result) {} + + public void onLoadChildren(String parentId, Result> result, Bundle options) {} + + public void onLoadItem(String itemId, Result result) {} + + public void onSearch(String query, Bundle extras, Result> result) {} + + public void onCustomAction(String action, Bundle extras, Result result) {} + } + + private static class RemoteMediaBrowserServiceCompatStub + extends IRemoteMediaBrowserServiceCompat.Stub { + @Override + public void setProxyForTest(String testName) throws RemoteException { + switch (testName) { + case TEST_CONNECT_REJECTED: + setProxyForTestConnectRejected(); + break; + case TEST_ON_CHILDREN_CHANGED_SUBSCRIBE_AND_UNSUBSCRIBE: + setProxyForTestOnChildrenChanged_subscribeAndUnsubscribe(); + break; + default: + throw new IllegalArgumentException("Unknown testName: " + testName); + } + } + + @Override + public void notifyChildrenChanged(String parentId) throws RemoteException { + getInstance().notifyChildrenChanged(parentId); + } + + private void setProxyForTestConnectRejected() { + setMediaBrowserServiceProxy( + new MockMediaBrowserServiceCompat.Proxy() { + @Override + public BrowserRoot onGetRoot( + String clientPackageName, int clientUid, Bundle rootHints) { + return null; + } + }); + } + + private void setProxyForTestOnChildrenChanged_subscribeAndUnsubscribe() { + setMediaBrowserServiceProxy( + new MockMediaBrowserServiceCompat.Proxy() { + @Override + public void onLoadChildren( + String parentId, Result> result, Bundle options) { + result.sendResult(Collections.emptyList()); + } + }); + } + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MockMediaLibraryService.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MockMediaLibraryService.java new file mode 100644 index 0000000000..51de4c12a9 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MockMediaLibraryService.java @@ -0,0 +1,405 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.LibraryResult.RESULT_ERROR_BAD_VALUE; +import static com.google.android.exoplayer2.session.LibraryResult.RESULT_SUCCESS; +import static com.google.android.exoplayer2.session.MediaTestUtils.assertLibraryParamsEquals; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.CUSTOM_ACTION; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.CUSTOM_ACTION_ASSERT_PARAMS; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.CUSTOM_ACTION_EXTRAS; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.GET_CHILDREN_RESULT; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.LONG_LIST_COUNT; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.MEDIA_ID_GET_ITEM; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.MEDIA_ID_GET_NULL_ITEM; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED_EXTRAS; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED_ITEM_COUNT; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.PARENT_ID; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.PARENT_ID_ERROR; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.PARENT_ID_LONG_LIST; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.ROOT_EXTRAS; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.ROOT_ID; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.SEARCH_QUERY; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.SEARCH_QUERY_EMPTY_RESULT; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.SEARCH_QUERY_LONG_LIST; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.SEARCH_QUERY_TAKES_TIME; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.SEARCH_RESULT; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.SEARCH_RESULT_COUNT; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.SEARCH_TIME_IN_MS; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ALL; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ALL_WITH_NON_SUBSCRIBED_ID; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE; +import static com.google.android.exoplayer2.session.vct.common.MediaBrowserConstants.SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE_WITH_NON_SUBSCRIBED_ID; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.app.Service; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.os.HandlerThread; +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaMetadata; +import com.google.android.exoplayer2.session.MediaLibraryService.MediaLibrarySession.MediaLibrarySessionCallback; +import com.google.android.exoplayer2.session.MediaSession.ControllerInfo; +import com.google.android.exoplayer2.session.vct.common.TestHandler; +import com.google.android.exoplayer2.session.vct.common.TestUtils; +import com.google.android.exoplayer2.util.Log; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Executors; + +/** A mock MediaLibraryService */ +public class MockMediaLibraryService extends MediaLibraryService { + /** ID of the session that this service will create. */ + public static final String ID = "TestLibrary"; + + // TODO(b/180293668): Set browsable property to the media metadata. + public static final MediaItem ROOT_ITEM = + new MediaItem.Builder() + .setMediaId(ROOT_ID) + .setMediaMetadata(new MediaMetadata.Builder().build()) + .build(); + public static final LibraryParams ROOT_PARAMS = + new LibraryParams.Builder().setExtras(ROOT_EXTRAS).build(); + private static final LibraryParams NOTIFY_CHILDREN_CHANGED_PARAMS = + new LibraryParams.Builder().setExtras(NOTIFY_CHILDREN_CHANGED_EXTRAS).build(); + + private static final String TAG = "MockMediaLibrarySvc2"; + + @GuardedBy("MockMediaLibraryService.class") + private static boolean assertLibraryParams; + + @GuardedBy("MockMediaLibraryService.class") + private static LibraryParams expectedParams; + + MediaLibrarySession session; + TestHandler handler; + HandlerThread handlerThread; + + @Override + public void onCreate() { + TestServiceRegistry.getInstance().setServiceInstance(this); + super.onCreate(); + handlerThread = new HandlerThread(TAG); + handlerThread.start(); + handler = new TestHandler(handlerThread.getLooper()); + } + + @Override + public void onDestroy() { + super.onDestroy(); + synchronized (MockMediaLibraryService.class) { + assertLibraryParams = false; + expectedParams = null; + } + TestServiceRegistry.getInstance().cleanUp(); + if (Build.VERSION.SDK_INT >= 18) { + handler.getLooper().quitSafely(); + } else { + handler.getLooper().quit(); + } + } + + @Override + public MediaLibrarySession onGetSession(@NonNull ControllerInfo controllerInfo) { + TestServiceRegistry registry = TestServiceRegistry.getInstance(); + TestServiceRegistry.OnGetSessionHandler onGetSessionHandler = registry.getOnGetSessionHandler(); + if (onGetSessionHandler != null) { + return (MediaLibrarySession) onGetSessionHandler.onGetSession(controllerInfo); + } + + MockPlayer player = + new MockPlayer.Builder().setLatchCount(1).setApplicationLooper(handler.getLooper()).build(); + + MediaLibrarySessionCallback callback = registry.getSessionCallback(); + session = + new MediaLibrarySession.Builder( + MockMediaLibraryService.this, + player, + callback != null ? callback : new TestLibrarySessionCallback()) + .setId(ID) + .build(); + return session; + } + + /** + * This changes the visibility of {@link Service#attachBaseContext(Context)} to public. This is a + * workaround for creating {@link MediaLibrarySession} without starting a service. + */ + @Override + public void attachBaseContext(Context base) { + super.attachBaseContext(base); + } + + public static void setAssertLibraryParams(LibraryParams expectedParams) { + synchronized (MockMediaLibraryService.class) { + assertLibraryParams = true; + MockMediaLibraryService.expectedParams = expectedParams; + } + } + + private class TestLibrarySessionCallback extends MediaLibrarySessionCallback { + + @Override + @Nullable + public MediaSession.ConnectResult onConnect(MediaSession session, ControllerInfo controller) { + if (!SUPPORT_APP_PACKAGE_NAME.equals(controller.getPackageName())) { + return null; + } + MediaSession.ConnectResult connectResult = super.onConnect(session, controller); + SessionCommands.Builder builder = + new SessionCommands.Builder(connectResult.availableSessionCommands); + builder.add(new SessionCommand(CUSTOM_ACTION, null)); + builder.add(new SessionCommand(CUSTOM_ACTION_ASSERT_PARAMS, null)); + return new MediaSession.ConnectResult(builder.build(), connectResult.availablePlayerCommands); + } + + @Override + @NonNull + public ListenableFuture onGetLibraryRoot( + @NonNull MediaLibrarySession session, + @NonNull ControllerInfo browser, + LibraryParams params) { + assertLibraryParams(params); + return new LibraryResult(RESULT_SUCCESS, ROOT_ITEM, ROOT_PARAMS).asFuture(); + } + + @Override + @NonNull + public ListenableFuture onGetItem( + @NonNull MediaLibrarySession session, + @NonNull ControllerInfo browser, + @NonNull String mediaId) { + switch (mediaId) { + case MEDIA_ID_GET_ITEM: + return new LibraryResult( + RESULT_SUCCESS, createPlayableMediaItem(mediaId), /* params= */ null) + .asFuture(); + case MEDIA_ID_GET_NULL_ITEM: + return new LibraryResult(RESULT_SUCCESS).asFuture(); + } + return new LibraryResult(RESULT_ERROR_BAD_VALUE).asFuture(); + } + + @Override + @NonNull + public ListenableFuture onGetChildren( + @NonNull MediaLibrarySession session, + @NonNull ControllerInfo browser, + @NonNull String parentId, + int page, + int pageSize, + LibraryParams params) { + assertLibraryParams(params); + if (PARENT_ID.equals(parentId)) { + return new LibraryResult( + RESULT_SUCCESS, + getPaginatedResult(GET_CHILDREN_RESULT, page, pageSize), + /* params= */ null) + .asFuture(); + } else if (PARENT_ID_LONG_LIST.equals(parentId)) { + List list = new ArrayList<>(LONG_LIST_COUNT); + for (int i = 0; i < LONG_LIST_COUNT; i++) { + list.add(createPlayableMediaItem(TestUtils.getMediaIdInFakeTimeline(i))); + } + return new LibraryResult(RESULT_SUCCESS, list, /* params= */ null).asFuture(); + } else if (PARENT_ID_ERROR.equals(parentId)) { + return new LibraryResult(RESULT_ERROR_BAD_VALUE).asFuture(); + } + // Includes the case of PARENT_ID_NO_CHILDREN. + return new LibraryResult(RESULT_SUCCESS, Collections.emptyList(), /* params= */ null) + .asFuture(); + } + + @Override + @SuppressWarnings("FutureReturnValueIgnored") + @NonNull + public ListenableFuture onSearch( + @NonNull MediaLibrarySession session, + @NonNull ControllerInfo browser, + @NonNull String query, + LibraryParams params) { + assertLibraryParams(params); + if (SEARCH_QUERY.equals(query)) { + MockMediaLibraryService.this.session.notifySearchResultChanged( + browser, query, SEARCH_RESULT_COUNT, params); + } else if (SEARCH_QUERY_LONG_LIST.equals(query)) { + MockMediaLibraryService.this.session.notifySearchResultChanged( + browser, query, LONG_LIST_COUNT, params); + } else if (SEARCH_QUERY_TAKES_TIME.equals(query)) { + // Searching takes some time. Notify after 5 seconds. + Executors.newSingleThreadScheduledExecutor() + .schedule( + new Runnable() { + @Override + public void run() { + MockMediaLibraryService.this.session.notifySearchResultChanged( + browser, query, SEARCH_RESULT_COUNT, params); + } + }, + SEARCH_TIME_IN_MS, + MILLISECONDS); + } else { + // SEARCH_QUERY_EMPTY_RESULT and SEARCH_QUERY_ERROR will be handled here. + MockMediaLibraryService.this.session.notifySearchResultChanged(browser, query, 0, params); + } + return new LibraryResult(RESULT_SUCCESS).asFuture(); + } + + @Override + @NonNull + public ListenableFuture onGetSearchResult( + @NonNull MediaLibrarySession session, + @NonNull ControllerInfo browser, + @NonNull String query, + int page, + int pageSize, + LibraryParams params) { + assertLibraryParams(params); + if (SEARCH_QUERY.equals(query)) { + return new LibraryResult( + RESULT_SUCCESS, + getPaginatedResult(SEARCH_RESULT, page, pageSize), + /* params= */ null) + .asFuture(); + } else if (SEARCH_QUERY_LONG_LIST.equals(query)) { + List list = new ArrayList<>(LONG_LIST_COUNT); + for (int i = 0; i < LONG_LIST_COUNT; i++) { + list.add(createPlayableMediaItem(TestUtils.getMediaIdInFakeTimeline(i))); + } + return new LibraryResult(RESULT_SUCCESS, list, /* params= */ null).asFuture(); + } else if (SEARCH_QUERY_EMPTY_RESULT.equals(query)) { + return new LibraryResult(RESULT_SUCCESS, Collections.emptyList(), /* params= */ null) + .asFuture(); + } else { + // SEARCH_QUERY_ERROR will be handled here. + return new LibraryResult(RESULT_ERROR_BAD_VALUE).asFuture(); + } + } + + @Override + @NonNull + public ListenableFuture onSubscribe( + @NonNull MediaLibrarySession session, + @NonNull ControllerInfo browser, + @NonNull String parentId, + LibraryParams params) { + assertLibraryParams(params); + String unsubscribedId = "unsubscribedId"; + switch (parentId) { + case SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ALL: + MockMediaLibraryService.this.session.notifyChildrenChanged( + parentId, NOTIFY_CHILDREN_CHANGED_ITEM_COUNT, NOTIFY_CHILDREN_CHANGED_PARAMS); + return new LibraryResult(RESULT_SUCCESS).asFuture(); + case SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE: + MockMediaLibraryService.this.session.notifyChildrenChanged( + MediaTestUtils.getTestControllerInfo(MockMediaLibraryService.this.session), + parentId, + NOTIFY_CHILDREN_CHANGED_ITEM_COUNT, + NOTIFY_CHILDREN_CHANGED_PARAMS); + return new LibraryResult(RESULT_SUCCESS).asFuture(); + case SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ALL_WITH_NON_SUBSCRIBED_ID: + MockMediaLibraryService.this.session.notifyChildrenChanged( + unsubscribedId, NOTIFY_CHILDREN_CHANGED_ITEM_COUNT, NOTIFY_CHILDREN_CHANGED_PARAMS); + return new LibraryResult(RESULT_SUCCESS).asFuture(); + case SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE_WITH_NON_SUBSCRIBED_ID: + MockMediaLibraryService.this.session.notifyChildrenChanged( + MediaTestUtils.getTestControllerInfo(MockMediaLibraryService.this.session), + unsubscribedId, + NOTIFY_CHILDREN_CHANGED_ITEM_COUNT, + NOTIFY_CHILDREN_CHANGED_PARAMS); + return new LibraryResult(RESULT_SUCCESS).asFuture(); + } + return new LibraryResult(RESULT_ERROR_BAD_VALUE).asFuture(); + } + + @Override + @NonNull + public ListenableFuture onCustomCommand( + @NonNull MediaSession session, + @NonNull ControllerInfo controller, + @NonNull SessionCommand sessionCommand, + Bundle args) { + switch (sessionCommand.customAction) { + case CUSTOM_ACTION: + return new SessionResult(RESULT_SUCCESS, CUSTOM_ACTION_EXTRAS).asFuture(); + case CUSTOM_ACTION_ASSERT_PARAMS: + LibraryParams params = + BundleableUtils.fromNullableBundle( + LibraryParams.CREATOR, args.getBundle(CUSTOM_ACTION_ASSERT_PARAMS)); + setAssertLibraryParams(params); + return new SessionResult(RESULT_SUCCESS).asFuture(); + } + return new SessionResult(RESULT_ERROR_BAD_VALUE).asFuture(); + } + + private void assertLibraryParams(LibraryParams params) { + synchronized (MockMediaLibraryService.class) { + if (assertLibraryParams) { + assertLibraryParamsEquals(expectedParams, params); + } + } + } + } + + private List getPaginatedResult(List items, int page, int pageSize) { + if (items == null) { + return null; + } else if (items.size() == 0) { + return new ArrayList<>(); + } + + int totalItemCount = items.size(); + int fromIndex = page * pageSize; + int toIndex = Math.min((page + 1) * pageSize, totalItemCount); + + List paginatedMediaIdList = new ArrayList<>(); + try { + // The case of (fromIndex >= totalItemCount) will throw exception below. + paginatedMediaIdList = items.subList(fromIndex, toIndex); + } catch (IndexOutOfBoundsException | IllegalArgumentException e) { + Log.d( + TAG, + "Result is empty for given pagination arguments: totalItemCount=" + + totalItemCount + + ", page=" + + page + + ", pageSize=" + + pageSize, + e); + } + + // Create a list of MediaItem from the list of media IDs. + List result = new ArrayList<>(); + for (int i = 0; i < paginatedMediaIdList.size(); i++) { + result.add(createPlayableMediaItem(paginatedMediaIdList.get(i))); + } + return result; + } + + private static MediaItem createPlayableMediaItem(String mediaId) { + // TODO(b/180293668): Set playable property to the media metadata. + MediaMetadata mediaMetadata = new MediaMetadata.Builder().build(); + return new MediaItem.Builder().setMediaId(mediaId).setMediaMetadata(mediaMetadata).build(); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MockMediaSessionService.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MockMediaSessionService.java new file mode 100644 index 0000000000..a4bf42838a --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MockMediaSessionService.java @@ -0,0 +1,87 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; + +import android.os.Build; +import android.os.HandlerThread; +import android.text.TextUtils; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.session.MediaSession.ControllerInfo; + +/** A mock MediaSessionService */ +public class MockMediaSessionService extends MediaSessionService { + /** ID of the session that this service will create. */ + public static final String ID = "TestSession"; + + public MediaSession session; + private HandlerThread handlerThread; + + @Override + public void onCreate() { + TestServiceRegistry.getInstance().setServiceInstance(this); + super.onCreate(); + handlerThread = new HandlerThread("MockMediaSessionService"); + handlerThread.start(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + TestServiceRegistry.getInstance().cleanUp(); + if (Build.VERSION.SDK_INT >= 18) { + handlerThread.quitSafely(); + } else { + handlerThread.quit(); + } + } + + @Override + public MediaSession onGetSession(@NonNull ControllerInfo controllerInfo) { + TestServiceRegistry registry = TestServiceRegistry.getInstance(); + TestServiceRegistry.OnGetSessionHandler onGetSessionHandler = registry.getOnGetSessionHandler(); + if (onGetSessionHandler != null) { + return onGetSessionHandler.onGetSession(controllerInfo); + } + + if (session == null) { + MediaSession.SessionCallback callback = registry.getSessionCallback(); + MockPlayer player = + new MockPlayer.Builder().setApplicationLooper(handlerThread.getLooper()).build(); + session = + new MediaSession.Builder(MockMediaSessionService.this, player) + .setId(ID) + .setSessionCallback(callback != null ? callback : new TestSessionCallback()) + .build(); + } + return session; + } + + private static class TestSessionCallback extends MediaSession.SessionCallback { + + @Nullable + @Override + public MediaSession.ConnectResult onConnect(MediaSession session, ControllerInfo controller) { + if (TextUtils.equals(SUPPORT_APP_PACKAGE_NAME, controller.getPackageName())) { + return super.onConnect(session, controller); + } + return null; + } + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MockPlayer.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MockPlayer.java new file mode 100644 index 0000000000..8c5aee85e0 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/MockPlayer.java @@ -0,0 +1,870 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.MediaUtils.createPlayerCommandsWithAllCommands; + +import android.os.Looper; +import android.view.Surface; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.collection.ArraySet; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaMetadata; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.audio.AudioAttributes; +import com.google.android.exoplayer2.device.DeviceInfo; +import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.VideoSize; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +/** A mock implementation of {@link SessionPlayer} for testing. */ +public class MockPlayer implements SessionPlayer { + + @NonNull public final CountDownLatch countDownLatch; + private final boolean changePlayerStateWithTransportControl; + @NonNull private final Looper applicationLooper; + private final ArraySet callbacks = new ArraySet<>(); + + @Nullable ExoPlaybackException playerError; + @NonNull public AudioAttributes audioAttributes; + public long seekPositionMs; + public int seekWindowIndex; + public long currentPosition; + public long bufferedPosition; + public long duration; + public int bufferedPercentage; + public long totalBufferedDuration; + public long currentLiveOffset; + public long contentPosition; + public long contentDuration; + public long contentBufferedPosition; + public boolean isPlayingAd; + public int currentAdGroupIndex; + public int currentAdIndexInAdGroup; + @Nullable public PlaybackParameters playbackParameters; + public Timeline timeline; + public List mediaItems; + public boolean resetPosition; + public int startWindowIndex; + public long startPositionMs; + public MediaMetadata playlistMetadata; + @Nullable public MediaItem currentMediaItem; + public int index; + public int fromIndex; + public int toIndex; + public int newIndex; + public int currentPeriodIndex; + public int currentWindowIndex; + @RepeatMode public int repeatMode; + public boolean shuffleModeEnabled; + public VideoSize videoSize; + @Nullable public Surface surface; + public float volume; + public DeviceInfo deviceInfo; + public int deviceVolume; + public boolean deviceMuted; + public boolean playWhenReady; + @PlaybackSuppressionReason public int playbackSuppressionReason; + @State public int playbackState; + public boolean isPlaying; + public boolean isLoading; + public Commands commands; + + public boolean playCalled; + public boolean pauseCalled; + public boolean prepareCalled; + public boolean stopCalled; + public boolean releaseCalled; + public boolean seekToDefaultPositionCalled; + public boolean seekToDefaultPositionWithWindowIndexCalled; + public boolean seekToCalled; + public boolean seekToWithWindowIndexCalled; + public boolean setPlaybackSpeedCalled; + public boolean setPlaybackParametersCalled; + public boolean setMediaItemsCalled; + public boolean setPlaylistMetadataCalled; + public boolean addMediaItemsCalled; + public boolean removeMediaItemsCalled; + public boolean moveMediaItemsCalled; + public boolean previousCalled; + public boolean nextCalled; + public boolean setRepeatModeCalled; + public boolean setShuffleModeCalled; + public boolean setVolumeCalled; + public boolean setDeviceVolumeCalled; + public boolean increaseDeviceVolumeCalled; + public boolean decreaseDeviceVolumeCalled; + public boolean setDeviceMutedCalled; + public boolean setPlayWhenReadyCalled; + + private MockPlayer(Builder builder) { + countDownLatch = new CountDownLatch(builder.latchCount); + changePlayerStateWithTransportControl = builder.changePlayerStateWithTransportControl; + applicationLooper = builder.applicationLooper; + + playbackParameters = PlaybackParameters.DEFAULT; + + // Sets default audio attributes to prevent setVolume() from being called with the play(). + audioAttributes = AudioAttributes.DEFAULT; + + timeline = Timeline.EMPTY; + playlistMetadata = MediaMetadata.EMPTY; + index = C.INDEX_UNSET; + fromIndex = C.INDEX_UNSET; + toIndex = C.INDEX_UNSET; + currentPeriodIndex = C.INDEX_UNSET; + currentWindowIndex = C.INDEX_UNSET; + repeatMode = Player.REPEAT_MODE_OFF; + videoSize = VideoSize.UNKNOWN; + volume = 1.0f; + deviceInfo = DeviceInfo.UNKNOWN; + seekPositionMs = C.TIME_UNSET; + seekWindowIndex = C.INDEX_UNSET; + + currentPosition = C.TIME_UNSET; + duration = C.TIME_UNSET; + currentLiveOffset = C.TIME_UNSET; + contentDuration = C.TIME_UNSET; + contentPosition = C.TIME_UNSET; + contentBufferedPosition = C.TIME_UNSET; + currentAdGroupIndex = C.INDEX_UNSET; + currentAdIndexInAdGroup = C.INDEX_UNSET; + + // Invalid playbackState throws assertion error. + playbackState = Player.STATE_IDLE; + + commands = createPlayerCommandsWithAllCommands(); + } + + @Override + public void release() { + releaseCalled = true; + } + + @Override + public void stop() { + stopCalled = true; + countDownLatch.countDown(); + } + + @Deprecated + @Override + public void stop(boolean reset) { + throw new UnsupportedOperationException(); + } + + @Override + public void addListener(@NonNull PlayerCallback callback) { + callbacks.add(callback); + } + + @Override + public void removeListener(@NonNull PlayerCallback callback) { + callbacks.remove(callback); + } + + @Override + @Nullable + public ExoPlaybackException getPlayerError() { + return playerError; + } + + @Override + public void play() { + playCalled = true; + countDownLatch.countDown(); + if (changePlayerStateWithTransportControl) { + notifyPlayWhenReadyChanged( + /* playWhenReady= */ true, Player.PLAYBACK_SUPPRESSION_REASON_NONE); + } + } + + @Override + public void pause() { + pauseCalled = true; + countDownLatch.countDown(); + if (changePlayerStateWithTransportControl) { + notifyPlayWhenReadyChanged( + /* playWhenReady= */ false, Player.PLAYBACK_SUPPRESSION_REASON_NONE); + } + } + + @Override + public void prepare() { + prepareCalled = true; + countDownLatch.countDown(); + if (changePlayerStateWithTransportControl) { + notifyPlaybackStateChanged(Player.STATE_READY); + } + } + + @Override + public void seekToDefaultPosition() { + seekToDefaultPositionCalled = true; + countDownLatch.countDown(); + } + + @Override + public void seekToDefaultPosition(int windowIndex) { + seekToDefaultPositionWithWindowIndexCalled = true; + seekWindowIndex = windowIndex; + countDownLatch.countDown(); + } + + @Override + public void seekTo(long positionMs) { + seekToCalled = true; + seekPositionMs = positionMs; + countDownLatch.countDown(); + } + + @Override + public void seekTo(int windowIndex, long positionMs) { + seekToWithWindowIndexCalled = true; + seekWindowIndex = windowIndex; + seekPositionMs = positionMs; + countDownLatch.countDown(); + } + + @Override + public long getCurrentPosition() { + return currentPosition; + } + + @Override + public long getBufferedPosition() { + return bufferedPosition; + } + + @Override + public boolean isPlayingAd() { + return isPlayingAd; + } + + @Override + public int getCurrentAdGroupIndex() { + return currentAdGroupIndex; + } + + @Override + public int getCurrentAdIndexInAdGroup() { + return currentAdIndexInAdGroup; + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return playbackParameters == null ? PlaybackParameters.DEFAULT : playbackParameters; + } + + @Override + public long getDuration() { + return duration; + } + + @Override + public int getBufferedPercentage() { + return bufferedPercentage; + } + + @Override + public long getTotalBufferedDuration() { + return totalBufferedDuration; + } + + @Override + public long getCurrentLiveOffset() { + return currentLiveOffset; + } + + @Override + public long getContentDuration() { + return contentDuration; + } + + @Override + public long getContentPosition() { + return contentPosition; + } + + @Override + public long getContentBufferedPosition() { + return contentBufferedPosition; + } + + @Override + public boolean isCommandAvailable(int command) { + return commands.contains(command); + } + + @Override + public Commands getAvailableCommands() { + return commands; + } + + public void notifyPlayerError(@Nullable ExoPlaybackException playerError) { + if (this.playerError == playerError) { + return; + } + this.playerError = playerError; + // TODO(b/184262323): Remove this check when migrating to onPlayerErrorChanged() that takes + // nullable error. + if (playerError == null) { + return; + } + for (PlayerCallback callback : callbacks) { + callback.onPlayerError(playerError); + } + } + + public void notifyAvailableCommandsChanged(Commands commands) { + if (Util.areEqual(this.commands, commands)) { + return; + } + this.commands = commands; + for (PlayerCallback callback : callbacks) { + callback.onAvailableCommandsChanged(commands); + } + } + + public void notifyPlayWhenReadyChanged( + boolean playWhenReady, @PlaybackSuppressionReason int reason) { + boolean playWhenReadyChanged = (this.playWhenReady != playWhenReady); + boolean playbackSuppressionReasonChanged = (this.playbackSuppressionReason != reason); + if (!playWhenReadyChanged && !playbackSuppressionReasonChanged) { + return; + } + + this.playWhenReady = playWhenReady; + this.playbackSuppressionReason = reason; + for (PlayerCallback callback : callbacks) { + if (playWhenReadyChanged) { + callback.onPlayWhenReadyChanged( + playWhenReady, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); + } + if (playbackSuppressionReasonChanged) { + callback.onPlaybackSuppressionReasonChanged(reason); + } + } + } + + public void notifyPlaybackStateChanged(@State int playbackState) { + if (this.playbackState == playbackState) { + return; + } + this.playbackState = playbackState; + for (PlayerCallback callback : callbacks) { + callback.onPlaybackStateChanged(playbackState); + } + } + + public void notifyIsPlayingChanged(boolean isPlaying) { + if (this.isPlaying == isPlaying) { + return; + } + this.isPlaying = isPlaying; + for (PlayerCallback callback : callbacks) { + callback.onIsPlayingChanged(isPlaying); + } + } + + public void notifyIsLoadingChanged(boolean isLoading) { + if (this.isLoading == isLoading) { + return; + } + this.isLoading = isLoading; + for (PlayerCallback callback : callbacks) { + callback.onIsLoadingChanged(isLoading); + } + } + + public void notifyPositionDiscontinuity( + PositionInfo oldPosition, PositionInfo newPosition, @DiscontinuityReason int reason) { + for (PlayerCallback callback : callbacks) { + callback.onPositionDiscontinuity(oldPosition, newPosition, reason); + } + } + + public void notifyMediaItemTransition( + @Nullable MediaItem mediaItem, @MediaItemTransitionReason int reason) { + for (PlayerCallback callback : callbacks) { + callback.onMediaItemTransition(mediaItem, reason); + } + } + + public void notifyPlaybackParametersChanged(PlaybackParameters playbackParameters) { + if (Util.areEqual(this.playbackParameters, playbackParameters)) { + return; + } + this.playbackParameters = playbackParameters; + for (PlayerCallback callback : callbacks) { + callback.onPlaybackParametersChanged( + playbackParameters == null ? PlaybackParameters.DEFAULT : playbackParameters); + } + } + + public void notifyAudioAttributesChanged(@NonNull AudioAttributes attrs) { + for (PlayerCallback callback : callbacks) { + callback.onAudioAttributesChanged(attrs); + } + } + + @Override + @NonNull + public AudioAttributes getAudioAttributes() { + return audioAttributes; + } + + @Override + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + setPlaybackParametersCalled = true; + this.playbackParameters = playbackParameters; + countDownLatch.countDown(); + } + + @Override + public void setPlaybackSpeed(float speed) { + setPlaybackSpeedCalled = true; + playbackParameters = new PlaybackParameters(speed); + countDownLatch.countDown(); + } + + @Override + public float getVolume() { + return volume; + } + + @Override + public void setVolume(float volume) { + setVolumeCalled = true; + this.volume = volume; + countDownLatch.countDown(); + } + + @Override + @NonNull + public DeviceInfo getDeviceInfo() { + return deviceInfo; + } + + @Override + public int getDeviceVolume() { + return deviceVolume; + } + + @Override + public boolean isDeviceMuted() { + return deviceMuted; + } + + @Override + public void setDeviceVolume(int volume) { + setDeviceVolumeCalled = true; + deviceVolume = volume; + countDownLatch.countDown(); + } + + @Override + public void increaseDeviceVolume() { + increaseDeviceVolumeCalled = true; + countDownLatch.countDown(); + } + + @Override + public void decreaseDeviceVolume() { + decreaseDeviceVolumeCalled = true; + countDownLatch.countDown(); + } + + @Override + public void setDeviceMuted(boolean muted) { + setDeviceMutedCalled = true; + deviceMuted = muted; + countDownLatch.countDown(); + } + + @Override + public void setPlayWhenReady(boolean playWhenReady) { + this.setPlayWhenReadyCalled = true; + this.playWhenReady = playWhenReady; + countDownLatch.countDown(); + } + + @Override + public boolean getPlayWhenReady() { + return playWhenReady; + } + + @Override + @PlaybackSuppressionReason + public int getPlaybackSuppressionReason() { + return playbackSuppressionReason; + } + + @Override + @State + public int getPlaybackState() { + return playbackState; + } + + @Override + public boolean isPlaying() { + return isPlaying; + } + + @Override + public boolean isLoading() { + return isLoading; + } + + ///////////////////////////////////////////////////////////////////////////////// + // Playlist APIs + ///////////////////////////////////////////////////////////////////////////////// + + @Override + public Object getCurrentManifest() { + throw new UnsupportedOperationException(); + } + + @Override + public Timeline getCurrentTimeline() { + return timeline; + } + + @Override + public void setMediaItem(MediaItem mediaItem) { + setMediaItems(Collections.singletonList(mediaItem)); + } + + @Override + public void setMediaItem(MediaItem mediaItem, long startPositionMs) { + setMediaItems(Collections.singletonList(mediaItem), /* startWindowIndex= */ 0, startPositionMs); + } + + @Override + public void setMediaItem(MediaItem mediaItem, boolean resetPosition) { + setMediaItems(Collections.singletonList(mediaItem), resetPosition); + } + + @Override + public void setMediaItems(List mediaItems) { + setMediaItems(mediaItems, /* resetPosition= */ true); + } + + @Override + public void setMediaItems(@NonNull List mediaItems, boolean resetPosition) { + setMediaItemsCalled = true; + this.mediaItems = mediaItems; + this.resetPosition = resetPosition; + countDownLatch.countDown(); + } + + @Override + public void setMediaItems( + List mediaItems, int startWindowIndex, long startPositionMs) { + setMediaItemsCalled = true; + this.mediaItems = mediaItems; + this.startWindowIndex = startWindowIndex; + this.startPositionMs = startPositionMs; + countDownLatch.countDown(); + } + + @Override + public MediaMetadata getPlaylistMetadata() { + return playlistMetadata; + } + + @Override + public void setPlaylistMetadata(MediaMetadata playlistMetadata) { + setPlaylistMetadataCalled = true; + this.playlistMetadata = playlistMetadata; + countDownLatch.countDown(); + } + + @Override + public boolean isCurrentWindowDynamic() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isCurrentWindowLive() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isCurrentWindowSeekable() { + throw new UnsupportedOperationException(); + } + + @Override + @Nullable + public MediaItem getCurrentMediaItem() { + return currentMediaItem; + } + + @Override + public int getMediaItemCount() { + throw new UnsupportedOperationException(); + } + + @Override + public MediaItem getMediaItemAt(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentPeriodIndex() { + return currentPeriodIndex; + } + + @Override + public int getCurrentWindowIndex() { + return currentWindowIndex; + } + + @Override + public int getPreviousWindowIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int getNextWindowIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public void addMediaItem(MediaItem mediaItem) { + addMediaItems(Collections.singletonList(mediaItem)); + } + + @Override + public void addMediaItem(int index, MediaItem mediaItem) { + addMediaItems(index, Collections.singletonList(mediaItem)); + } + + @Override + public void addMediaItems(List mediaItems) { + addMediaItems(/* index= */ Integer.MAX_VALUE, mediaItems); + } + + @Override + public void addMediaItems(int index, List mediaItems) { + addMediaItemsCalled = true; + this.index = index; + this.mediaItems = mediaItems; + countDownLatch.countDown(); + } + + @Override + public void removeMediaItem(int index) { + removeMediaItems(/* fromIndex= */ index, /* toIndex= */ index + 1); + } + + @Override + public void removeMediaItems(int fromIndex, int toIndex) { + removeMediaItemsCalled = true; + this.fromIndex = fromIndex; + this.toIndex = toIndex; + countDownLatch.countDown(); + } + + @Override + public void clearMediaItems() { + removeMediaItems(/* fromIndex= */ 0, /* toIndex= */ Integer.MAX_VALUE); + } + + @Override + public void moveMediaItem(int currentIndex, int newIndex) { + if (currentIndex != newIndex) { + moveMediaItems(/* fromIndex= */ currentIndex, /* toIndex= */ currentIndex + 1, newIndex); + } + } + + @Override + public void moveMediaItems(int fromIndex, int toIndex, int newIndex) { + moveMediaItemsCalled = true; + this.fromIndex = fromIndex; + this.toIndex = toIndex; + this.newIndex = newIndex; + countDownLatch.countDown(); + } + + @Override + public boolean hasPrevious() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasNext() { + throw new UnsupportedOperationException(); + } + + @Override + public void previous() { + previousCalled = true; + countDownLatch.countDown(); + } + + @Override + public void next() { + nextCalled = true; + countDownLatch.countDown(); + } + + @Override + public int getRepeatMode() { + return repeatMode; + } + + @Override + public void setRepeatMode(int repeatMode) { + setRepeatModeCalled = true; + this.repeatMode = repeatMode; + countDownLatch.countDown(); + } + + @Override + public boolean getShuffleModeEnabled() { + return shuffleModeEnabled; + } + + @Override + public void setShuffleModeEnabled(boolean shuffleModeEnabled) { + setShuffleModeCalled = true; + this.shuffleModeEnabled = shuffleModeEnabled; + countDownLatch.countDown(); + } + + public void notifyShuffleModeEnabledChanged() { + boolean shuffleModeEnabled = this.shuffleModeEnabled; + for (PlayerCallback callback : callbacks) { + callback.onShuffleModeEnabledChanged(shuffleModeEnabled); + } + } + + public void notifyRepeatModeChanged() { + int repeatMode = this.repeatMode; + for (PlayerCallback callback : callbacks) { + callback.onRepeatModeChanged(repeatMode); + } + } + + public void notifyTimelineChanged(@TimelineChangeReason int reason) { + Timeline timeline = this.timeline; + for (PlayerCallback callback : callbacks) { + callback.onTimelineChanged(timeline, reason); + } + } + + public void notifyPlaylistMetadataChanged() { + MediaMetadata metadata = playlistMetadata; + for (PlayerCallback callback : callbacks) { + callback.onPlaylistMetadataChanged(metadata); + } + } + + @Override + @NonNull + public VideoSize getVideoSize() { + if (videoSize == null) { + videoSize = VideoSize.UNKNOWN; + } + return videoSize; + } + + public void notifyVideoSizeChanged(@NonNull VideoSize videoSize) { + for (PlayerCallback callback : callbacks) { + callback.onVideoSizeChanged(videoSize); + } + } + + @Override + public void clearVideoSurface() { + surface = null; + } + + @Override + public void clearVideoSurface(@Nullable Surface surface) { + if (surface != null && surface == this.surface) { + this.surface = null; + } + } + + @Override + public void setVideoSurface(@Nullable Surface surface) { + this.surface = surface; + } + + public boolean surfaceExists() { + return surface != null; + } + + public void notifyDeviceVolumeChanged() { + for (PlayerCallback callback : callbacks) { + callback.onDeviceVolumeChanged(deviceVolume, deviceMuted); + } + } + + public void notifyDeviceInfoChanged() { + for (PlayerCallback callback : callbacks) { + callback.onDeviceInfoChanged(deviceInfo); + } + } + + @Override + @NonNull + public Looper getApplicationLooper() { + return applicationLooper; + } + + /** Builder for {@link MockPlayer}. */ + public static final class Builder { + + private int latchCount; + private boolean changePlayerStateWithTransportControl; + private Looper applicationLooper; + + public Builder() { + applicationLooper = Util.getCurrentOrMainLooper(); + } + + public Builder setLatchCount(int latchCount) { + this.latchCount = latchCount; + return this; + } + + public Builder setChangePlayerStateWithTransportControl( + boolean changePlayerStateWithTransportControl) { + this.changePlayerStateWithTransportControl = changePlayerStateWithTransportControl; + return this; + } + + public Builder setApplicationLooper(Looper applicationLooper) { + this.applicationLooper = applicationLooper; + return this; + } + + public MockPlayer build() { + return new MockPlayer(this); + } + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/PlaylistTimeline.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/PlaylistTimeline.java new file mode 100644 index 0000000000..f221e3ac5d --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/PlaylistTimeline.java @@ -0,0 +1,158 @@ +/* + * Copyright 2021 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.session; + +import static com.google.android.exoplayer2.util.Assertions.checkState; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.common.collect.ImmutableList; +import java.util.Arrays; +import java.util.List; + +/** A {@link Timeline} implementation for testing session/controller(s). */ +public class PlaylistTimeline extends Timeline { + + private static final long DEFAULT_DURATION_MS = 100; + + private final ImmutableList mediaItems; + private final int[] shuffledIndices; + private final int[] indicesInShuffled; + + public PlaylistTimeline(List mediaItems) { + this(mediaItems, createUnshuffledIndices(mediaItems.size())); + } + + public PlaylistTimeline(List mediaItems, int[] shuffledIndices) { + checkState(mediaItems.size() == shuffledIndices.length); + this.mediaItems = ImmutableList.copyOf(mediaItems); + this.shuffledIndices = Arrays.copyOf(shuffledIndices, shuffledIndices.length); + indicesInShuffled = new int[shuffledIndices.length]; + for (int i = 0; i < shuffledIndices.length; i++) { + indicesInShuffled[shuffledIndices[i]] = i; + } + } + + @Override + public int getWindowCount() { + return mediaItems.size(); + } + + @Override + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { + window.set( + /* uid= */ 0, + mediaItems.get(windowIndex), + /* manifest= */ null, + /* presentationStartTimeMs= */ 0, + /* windowStartTimeMs= */ 0, + /* elapsedRealtimeEpochOffsetMs= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* liveConfiguration= */ null, + /* defaultPositionUs= */ 0, + /* durationUs= */ C.msToUs(DEFAULT_DURATION_MS), + /* firstPeriodIndex= */ windowIndex, + /* lastPeriodIndex= */ windowIndex, + /* positionInFirstPeriodUs= */ 0); + window.isPlaceholder = false; + return window; + } + + @Override + public int getNextWindowIndex( + int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { + if (repeatMode == Player.REPEAT_MODE_ONE) { + return windowIndex; + } + if (windowIndex == getLastWindowIndex(shuffleModeEnabled)) { + return repeatMode == Player.REPEAT_MODE_ALL + ? getFirstWindowIndex(shuffleModeEnabled) + : C.INDEX_UNSET; + } + return shuffleModeEnabled + ? shuffledIndices[indicesInShuffled[windowIndex] + 1] + : windowIndex + 1; + } + + @Override + public int getPreviousWindowIndex( + int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { + if (repeatMode == Player.REPEAT_MODE_ONE) { + return windowIndex; + } + if (windowIndex == getFirstWindowIndex(shuffleModeEnabled)) { + return repeatMode == Player.REPEAT_MODE_ALL + ? getLastWindowIndex(shuffleModeEnabled) + : C.INDEX_UNSET; + } + return shuffleModeEnabled + ? shuffledIndices[indicesInShuffled[windowIndex] - 1] + : windowIndex - 1; + } + + @Override + public int getLastWindowIndex(boolean shuffleModeEnabled) { + if (isEmpty()) { + return C.INDEX_UNSET; + } + return shuffleModeEnabled ? shuffledIndices[getWindowCount() - 1] : getWindowCount() - 1; + } + + @Override + public int getFirstWindowIndex(boolean shuffleModeEnabled) { + if (isEmpty()) { + return C.INDEX_UNSET; + } + return shuffleModeEnabled ? shuffledIndices[0] : 0; + } + + @Override + public int getPeriodCount() { + return getWindowCount(); + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + period.set( + /* id= */ null, + /* uid= */ null, + periodIndex, + C.msToUs(DEFAULT_DURATION_MS), + /* positionInWindowUs= */ 0); + return period; + } + + @Override + public int getIndexOfPeriod(Object uid) { + throw new UnsupportedOperationException(); + } + + @Override + public Object getUidOfPeriod(int periodIndex) { + throw new UnsupportedOperationException(); + } + + private static int[] createUnshuffledIndices(int length) { + int[] indices = new int[length]; + for (int i = 0; i < length; i++) { + indices[i] = i; + } + return indices; + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/RemoteMediaBrowser.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/RemoteMediaBrowser.java new file mode 100644 index 0000000000..a1b7aa9c44 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/RemoteMediaBrowser.java @@ -0,0 +1,108 @@ +/* + * 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.google.android.exoplayer2.session; + +import android.content.Context; +import android.os.Bundle; +import android.os.RemoteException; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.session.MediaLibraryService.LibraryParams; + +/** + * Represents remote {@link MediaBrowser} the client app's MediaControllerService. Users can run + * {@link MediaBrowser} methods remotely with this object. + */ +public class RemoteMediaBrowser extends RemoteMediaController { + + /** + * Create a {@link MediaBrowser} in the client app. Should NOT be called main thread. + * + * @param waitForConnection true if the remote browser needs to wait for the connection, false + * otherwise. + * @param connectionHints connection hints + */ + public RemoteMediaBrowser( + Context context, SessionToken token, boolean waitForConnection, Bundle connectionHints) + throws RemoteException { + super(context, token, connectionHints, waitForConnection); + } + + /** {@link MediaBrowser} methods. */ + public LibraryResult getLibraryRoot(@Nullable LibraryParams params) throws RemoteException { + Bundle result = binder.getLibraryRoot(controllerId, BundleableUtils.toNullableBundle(params)); + return LibraryResult.CREATOR.fromBundle(result); + } + + public LibraryResult subscribe(@NonNull String parentId, @Nullable LibraryParams params) + throws RemoteException { + Bundle result = + binder.subscribe(controllerId, parentId, BundleableUtils.toNullableBundle(params)); + return LibraryResult.CREATOR.fromBundle(result); + } + + public LibraryResult unsubscribe(@NonNull String parentId) throws RemoteException { + Bundle result = binder.unsubscribe(controllerId, parentId); + return LibraryResult.CREATOR.fromBundle(result); + } + + public LibraryResult getChildren( + @NonNull String parentId, int page, int pageSize, @Nullable LibraryParams params) + throws RemoteException { + Bundle result = + binder.getChildren( + controllerId, parentId, page, pageSize, BundleableUtils.toNullableBundle(params)); + return LibraryResult.CREATOR.fromBundle(result); + } + + public LibraryResult getItem(@NonNull String mediaId) throws RemoteException { + Bundle result = binder.getItem(controllerId, mediaId); + return LibraryResult.CREATOR.fromBundle(result); + } + + public LibraryResult search(@NonNull String query, @Nullable LibraryParams params) + throws RemoteException { + Bundle result = binder.search(controllerId, query, BundleableUtils.toNullableBundle(params)); + return LibraryResult.CREATOR.fromBundle(result); + } + + public LibraryResult getSearchResult( + @NonNull String query, int page, int pageSize, @Nullable LibraryParams params) + throws RemoteException { + Bundle result = + binder.getSearchResult( + controllerId, query, page, pageSize, BundleableUtils.toNullableBundle(params)); + return LibraryResult.CREATOR.fromBundle(result); + } + + //////////////////////////////////////////////////////////////////////////////// + // Non-public methods + //////////////////////////////////////////////////////////////////////////////// + + /** + * Create a {@link MediaBrowser} in the client app. Should be used after successful connection + * through {@link #connect()}. + * + * @param connectionHints connection hints + * @param waitForConnection true if this method needs to wait for the connection, + */ + void create(SessionToken token, Bundle connectionHints, boolean waitForConnection) + throws RemoteException { + binder.create( + /* isBrowser= */ true, controllerId, token.toBundle(), connectionHints, waitForConnection); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/RemoteMediaBrowserCompat.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/RemoteMediaBrowserCompat.java new file mode 100644 index 0000000000..7fe7216a64 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/RemoteMediaBrowserCompat.java @@ -0,0 +1,187 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.ACTION_MEDIA_BROWSER_COMPAT; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.MEDIA_BROWSER_COMPAT_PROVIDER_SERVICE; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.SERVICE_CONNECTION_TIMEOUT_MS; +import static com.google.common.truth.Truth.assertWithMessage; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.support.v4.media.MediaBrowserCompat; +import com.google.android.exoplayer2.session.vct.common.IRemoteMediaBrowserCompat; +import com.google.android.exoplayer2.util.Log; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; + +/** + * Represents remote {@link MediaBrowserCompat} the client app's MediaBrowserCompatProviderService. + * Users can run {@link MediaBrowserCompat} methods remotely with this object. + */ +public class RemoteMediaBrowserCompat { + private static final String TAG = "RemoteMediaBrowserCompat"; + + private final String browserId; + private final Context context; + private final CountDownLatch countDownLatch; + + private ServiceConnection serviceConnection; + private IRemoteMediaBrowserCompat binder; + + /** Create a {@link MediaBrowserCompat} in the client app. Should NOT be called main thread. */ + public RemoteMediaBrowserCompat(Context context, ComponentName serviceComponent) + throws RemoteException { + this.context = context; + browserId = UUID.randomUUID().toString(); + countDownLatch = new CountDownLatch(1); + serviceConnection = new MyServiceConnection(); + if (!connectToService()) { + assertWithMessage("Failed to connect to the MediaBrowserCompatProviderService.").fail(); + } + create(serviceComponent); + } + + public void cleanUp() throws RemoteException { + disconnect(); + disconnectFromService(); + } + + //////////////////////////////////////////////////////////////////////////////// + // MediaBrowserCompat methods + //////////////////////////////////////////////////////////////////////////////// + + /** + * Connect to the given media browser service. + * + * @param waitForConnection true if the remote browser needs to wait for the connection, false + * otherwise. + */ + public void connect(boolean waitForConnection) throws RemoteException { + binder.connect(browserId, waitForConnection); + } + + public void disconnect() throws RemoteException { + binder.disconnect(browserId); + } + + public boolean isConnected() throws RemoteException { + return binder.isConnected(browserId); + } + + public ComponentName getServiceComponent() throws RemoteException { + return binder.getServiceComponent(browserId); + } + + public String getRoot() throws RemoteException { + return binder.getRoot(browserId); + } + + public Bundle getExtras() throws RemoteException { + return binder.getExtras(browserId); + } + + public Bundle getConnectedSessionToken() throws RemoteException { + return binder.getConnectedSessionToken(browserId); + } + + public void subscribe(String parentId, Bundle options) throws RemoteException { + binder.subscribe(browserId, parentId, options); + } + + public void unsubscribe(String parentId) throws RemoteException { + binder.unsubscribe(browserId, parentId); + } + + public void getItem(String mediaId) throws RemoteException { + binder.getItem(browserId, mediaId); + } + + public void search(String query, Bundle extras) throws RemoteException { + binder.search(browserId, query, extras); + } + + public void sendCustomAction(String action, Bundle extras) throws RemoteException { + binder.sendCustomAction(browserId, action, extras); + } + + //////////////////////////////////////////////////////////////////////////////// + // Non-public methods + //////////////////////////////////////////////////////////////////////////////// + + /** + * Connects to client app's MediaBrowserCompatProviderService. Should NOT be called main thread. + * + * @return true if connected successfully, false if failed to connect. + */ + private boolean connectToService() { + Intent intent = new Intent(ACTION_MEDIA_BROWSER_COMPAT); + intent.setComponent(MEDIA_BROWSER_COMPAT_PROVIDER_SERVICE); + + boolean bound = false; + try { + bound = context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); + } catch (Exception e) { + Log.e(TAG, "Failed to bind to the MediaBrowserCompatProviderService.", e); + } + + if (bound) { + try { + countDownLatch.await(SERVICE_CONNECTION_TIMEOUT_MS, MILLISECONDS); + } catch (InterruptedException e) { + Log.e(TAG, "InterruptedException while waiting for onServiceConnected.", e); + } + } + return binder != null; + } + + /** Disconnects from client app's MediaBrowserCompatProviderService. */ + private void disconnectFromService() { + if (serviceConnection != null) { + context.unbindService(serviceConnection); + serviceConnection = null; + } + } + + /** + * Create a {@link MediaBrowserCompat} in the client app. Should be used after successful + * connection through {@link #connectToService()}. + */ + private void create(ComponentName serviceComponent) throws RemoteException { + binder.create(browserId, serviceComponent); + } + + class MyServiceConnection implements ServiceConnection { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + Log.d(TAG, "Connected to client app's MediaBrowserCompatProviderService."); + binder = IRemoteMediaBrowserCompat.Stub.asInterface(service); + countDownLatch.countDown(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + Log.d(TAG, "Disconnected from client app's MediaBrowserCompatProviderService."); + } + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/RemoteMediaBrowserServiceCompat.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/RemoteMediaBrowserServiceCompat.java new file mode 100644 index 0000000000..ccd18d8845 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/RemoteMediaBrowserServiceCompat.java @@ -0,0 +1,100 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.MOCK_MEDIA_BROWSER_SERVICE_COMPAT; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.SERVICE_CONNECTION_TIMEOUT_MS; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.os.RemoteException; +import androidx.annotation.NonNull; +import com.google.android.exoplayer2.session.vct.common.IRemoteMediaBrowserServiceCompat; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +/** A client to control service app's MockMediaBrowserServiceCompat remotely. */ +public class RemoteMediaBrowserServiceCompat { + + private final Context context; + private final ServiceConnection serviceConnection; + private final CountDownLatch connectedLatch; + private AtomicReference binderRef = new AtomicReference<>(); + + public RemoteMediaBrowserServiceCompat(@NonNull Context context) { + this.context = context; + serviceConnection = new MyServiceConnection(); + connectedLatch = new CountDownLatch(1); + + Intent intent = new Intent(); + intent.setComponent(MOCK_MEDIA_BROWSER_SERVICE_COMPAT); + + boolean bound = context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); + if (!bound) { + throw new IllegalArgumentException("Could not bind to the service"); + } + + boolean connected; + try { + connected = connectedLatch.await(SERVICE_CONNECTION_TIMEOUT_MS, MILLISECONDS); + } catch (InterruptedException e) { + throw new IllegalStateException("Interrupted while waiting for connection", e); + } + if (!connected) { + throw new IllegalStateException("ServiceConnection was not made in time"); + } + } + + public void release() { + context.unbindService(serviceConnection); + } + + /** Calls MockMediaBrowserServiceCompat#setMediaBrowserServiceProxy for a specific test case. */ + public void setProxyForTest(@NonNull String testName) throws RemoteException { + getBinderOrThrow().setProxyForTest(testName); + } + + /** Calls MockMediaBrowserServiceCompat#notifyChildrenChanged. */ + public void notifyChildrenChanged(String parentId) throws RemoteException { + getBinderOrThrow().notifyChildrenChanged(parentId); + } + + @NonNull + private IRemoteMediaBrowserServiceCompat getBinderOrThrow() { + IRemoteMediaBrowserServiceCompat binder = binderRef.get(); + if (binder == null) { + throw new IllegalStateException("service is not connected"); + } + return binder; + } + + private class MyServiceConnection implements ServiceConnection { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + binderRef.set(IRemoteMediaBrowserServiceCompat.Stub.asInterface(service)); + connectedLatch.countDown(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + binderRef.set(null); + } + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/RemoteMediaController.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/RemoteMediaController.java new file mode 100644 index 0000000000..edcdab22d7 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/RemoteMediaController.java @@ -0,0 +1,310 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.ACTION_MEDIA2_CONTROLLER; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.MEDIA2_CONTROLLER_PROVIDER_SERVICE; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.SERVICE_CONNECTION_TIMEOUT_MS; +import static com.google.common.truth.Truth.assertWithMessage; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.net.Uri; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaMetadata; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player.RepeatMode; +import com.google.android.exoplayer2.Rating; +import com.google.android.exoplayer2.session.vct.common.IRemoteMediaController; +import com.google.android.exoplayer2.session.vct.common.TestUtils; +import com.google.android.exoplayer2.util.Log; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; + +/** + * Represents remote {@link MediaController} the client app's MediaControllerProviderService. Users + * can run {@link MediaController} methods remotely with this object. + */ +public class RemoteMediaController { + static final String TAG = "RemoteMediaController"; + + final String controllerId; + final Context context; + final CountDownLatch countDownLatch; + + ServiceConnection serviceConnection; + IRemoteMediaController binder; + + /** + * Create a {@link MediaController} in the client app. Should NOT be called main thread. + * + * @param connectionHints connection hints + * @param waitForConnection true if the remote controller needs to wait for the connection, + */ + public RemoteMediaController( + Context context, SessionToken token, Bundle connectionHints, boolean waitForConnection) + throws RemoteException { + this.context = context; + controllerId = UUID.randomUUID().toString(); + countDownLatch = new CountDownLatch(1); + serviceConnection = new MyServiceConnection(); + if (!connect()) { + assertWithMessage("Failed to connect to the MediaControllerProviderService.").fail(); + } + create(token, connectionHints, waitForConnection); + } + + public void cleanUp() throws RemoteException { + release(); + disconnect(); + } + + //////////////////////////////////////////////////////////////////////////////// + // MediaController methods + //////////////////////////////////////////////////////////////////////////////// + + public SessionToken getConnectedSessionToken() throws RemoteException { + return BundleableUtils.fromNullableBundle( + SessionToken.CREATOR, binder.getConnectedSessionToken(controllerId)); + } + + public void play() throws RemoteException { + binder.play(controllerId); + } + + public void pause() throws RemoteException { + binder.pause(controllerId); + } + + public void prepare() throws RemoteException { + binder.prepare(controllerId); + } + + public void setPlayWhenReady(boolean playWhenReady) throws RemoteException { + binder.setPlayWhenReady(controllerId, playWhenReady); + } + + public void seekToDefaultPosition() throws RemoteException { + binder.seekToDefaultPosition(controllerId); + } + + public void seekToDefaultPosition(int windowIndex) throws RemoteException { + binder.seekToDefaultPositionWithWindowIndex(controllerId, windowIndex); + } + + public void seekTo(long positionMs) throws RemoteException { + binder.seekTo(controllerId, positionMs); + } + + public void seekTo(int windowIndex, long positionMs) throws RemoteException { + binder.seekToWithWindowIndex(controllerId, windowIndex, positionMs); + } + + public void setPlaybackParameters(PlaybackParameters playbackParameters) throws RemoteException { + binder.setPlaybackParameters( + controllerId, BundleableUtils.toNullableBundle(playbackParameters)); + } + + public void setPlaybackSpeed(float speed) throws RemoteException { + binder.setPlaybackSpeed(controllerId, speed); + } + + public void setMediaItems(@NonNull List mediaItems) throws RemoteException { + setMediaItems(mediaItems, /* resetPosition= */ true); + } + + public void setMediaItems(@NonNull List mediaItems, boolean resetPosition) + throws RemoteException { + binder.setMediaItems1(controllerId, BundleableUtils.toBundleList(mediaItems), resetPosition); + } + + public void setMediaItems( + @NonNull List mediaItems, int startWindowIndex, long startPositionMs) + throws RemoteException { + binder.setMediaItems2( + controllerId, BundleableUtils.toBundleList(mediaItems), startWindowIndex, startPositionMs); + } + + /** + * Client app will automatically create a playlist of size {@param size}, and call + * MediaController#setMediaItems() with the list. + * + *

Each item's media ID will be {@link TestUtils#getMediaIdInFakeTimeline(int)}. + */ + public void createAndSetFakeMediaItems(int size) throws RemoteException { + binder.createAndSetFakeMediaItems(controllerId, size); + } + + public void setMediaUri(@NonNull Uri uri, @Nullable Bundle extras) throws RemoteException { + binder.setMediaUri(controllerId, uri, extras); + } + + public void setPlaylistMetadata(MediaMetadata playlistMetadata) throws RemoteException { + binder.setPlaylistMetadata(controllerId, playlistMetadata.toBundle()); + } + + public void addMediaItems(int index, @NonNull MediaItem mediaItem) throws RemoteException { + addMediaItems(index, Collections.singletonList(mediaItem)); + } + + public void addMediaItems(int index, @NonNull List mediaItems) throws RemoteException { + binder.addMediaItems(controllerId, index, BundleableUtils.toBundleList(mediaItems)); + } + + public void removeMediaItem(int index) throws RemoteException { + removeMediaItems(index, index + 1); + } + + public void removeMediaItems(int fromIndex, int toIndex) throws RemoteException { + binder.removeMediaItems(controllerId, fromIndex, toIndex); + } + + public void moveMediaItems(int fromIndex, int toIndex, int newIndex) throws RemoteException { + binder.moveMediaItems(controllerId, fromIndex, toIndex, newIndex); + } + + public void previous() throws RemoteException { + binder.previous(controllerId); + } + + public void next() throws RemoteException { + binder.next(controllerId); + } + + public void setShuffleModeEnabled(boolean shuffleModeEnabled) throws RemoteException { + binder.setShuffleModeEnabled(controllerId, shuffleModeEnabled); + } + + public void setRepeatMode(@RepeatMode int repeatMode) throws RemoteException { + binder.setRepeatMode(controllerId, repeatMode); + } + + public void setVolume(float volume) throws RemoteException { + binder.setVolume(controllerId, volume); + } + + public void setDeviceVolume(int volume) throws RemoteException { + binder.setDeviceVolume(controllerId, volume); + } + + public void increaseDeviceVolume() throws RemoteException { + binder.increaseDeviceVolume(controllerId); + } + + public void decreaseDeviceVolume() throws RemoteException { + binder.decreaseDeviceVolume(controllerId); + } + + public void setDeviceMuted(boolean muted) throws RemoteException { + binder.setDeviceMuted(controllerId, muted); + } + + public SessionResult sendCustomCommand(@NonNull SessionCommand command, @Nullable Bundle args) + throws RemoteException { + Bundle result = binder.sendCustomCommand(controllerId, command.toBundle(), args); + return SessionResult.CREATOR.fromBundle(result); + } + + public SessionResult setRating(@NonNull String mediaId, @NonNull Rating rating) + throws RemoteException { + Bundle result = binder.setRating(controllerId, mediaId, rating.toBundle()); + return SessionResult.CREATOR.fromBundle(result); + } + + public void release() throws RemoteException { + binder.release(controllerId); + } + + public void stop() throws RemoteException { + binder.stop(controllerId); + } + + //////////////////////////////////////////////////////////////////////////////// + // Non-public methods + //////////////////////////////////////////////////////////////////////////////// + + /** + * Connects to client app's MediaControllerProviderService. Should NOT be called main thread. + * + * @return true if connected successfully, false if failed to connect. + */ + private boolean connect() { + Intent intent = new Intent(ACTION_MEDIA2_CONTROLLER); + intent.setComponent(MEDIA2_CONTROLLER_PROVIDER_SERVICE); + + boolean bound = false; + try { + bound = context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); + } catch (Exception e) { + Log.e(TAG, "Failed to bind to the MediaControllerProviderService.", e); + } + + if (bound) { + try { + countDownLatch.await(SERVICE_CONNECTION_TIMEOUT_MS, MILLISECONDS); + } catch (InterruptedException e) { + Log.e(TAG, "InterruptedException while waiting for onServiceConnected.", e); + } + } + return binder != null; + } + + /** Disconnects from client app's MediaControllerProviderService. */ + private void disconnect() { + if (serviceConnection != null) { + context.unbindService(serviceConnection); + serviceConnection = null; + } + } + + /** + * Create a {@link MediaController} in the client app. Should be used after successful connection + * through {@link #connect()}. + * + * @param connectionHints connection hints + * @param waitForConnection true if this method needs to wait for the connection, + */ + void create(SessionToken token, Bundle connectionHints, boolean waitForConnection) + throws RemoteException { + binder.create( + /* isBrowser= */ false, controllerId, token.toBundle(), connectionHints, waitForConnection); + } + + class MyServiceConnection implements ServiceConnection { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + Log.d(TAG, "Connected to client app's MediaControllerProviderService."); + binder = IRemoteMediaController.Stub.asInterface(service); + countDownLatch.countDown(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + Log.d(TAG, "Disconnected from client app's MediaControllerProviderService."); + } + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/RemoteMediaControllerCompat.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/RemoteMediaControllerCompat.java new file mode 100644 index 0000000000..c22fd14790 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/RemoteMediaControllerCompat.java @@ -0,0 +1,289 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.ACTION_MEDIA_CONTROLLER_COMPAT; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_ARGUMENTS; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.MEDIA_CONTROLLER_COMPAT_PROVIDER_SERVICE; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.SERVICE_CONNECTION_TIMEOUT_MS; +import static com.google.common.truth.Truth.assertWithMessage; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.net.Uri; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Parcelable; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.support.v4.media.MediaDescriptionCompat; +import android.support.v4.media.RatingCompat; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import com.google.android.exoplayer2.session.vct.common.IRemoteMediaControllerCompat; +import com.google.android.exoplayer2.util.Log; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; + +/** + * Represents remote {@link MediaControllerCompat} the client app's + * MediaControllerCompatProviderService. + * + *

Users can run {@link MediaControllerCompat} methods remotely with this object. + */ +public class RemoteMediaControllerCompat { + static final String TAG = "RemoteMediaControllerCompat"; + + final String controllerId; + final Context context; + final CountDownLatch countDownLatch; + + ServiceConnection serviceConnection; + IRemoteMediaControllerCompat binder; + TransportControls transportControls; + + /** + * Create a {@link MediaControllerCompat} in the client app. Should NOT be called main thread. + * + * @param waitForConnection true if the remote controller needs to wait for the connection, false + * otherwise. + */ + public RemoteMediaControllerCompat( + Context context, MediaSessionCompat.Token token, boolean waitForConnection) + throws RemoteException { + this.context = context; + controllerId = UUID.randomUUID().toString(); + countDownLatch = new CountDownLatch(1); + serviceConnection = new MyServiceConnection(); + if (!connect()) { + assertWithMessage("Failed to connect to the MediaControllerCompatProviderService.").fail(); + } + create(token, waitForConnection); + } + + public void cleanUp() { + disconnect(); + } + + /** + * Gets {@link TransportControls} for interact with the remote MockPlayer. Users can run + * MockPlayer methods remotely with this object. + */ + public TransportControls getTransportControls() { + return transportControls; + } + + //////////////////////////////////////////////////////////////////////////////// + // MediaControllerCompat methods + //////////////////////////////////////////////////////////////////////////////// + + public void addQueueItem(MediaDescriptionCompat description) throws RemoteException { + binder.addQueueItem(controllerId, createBundleWithParcelable(description)); + } + + public void addQueueItem(MediaDescriptionCompat description, int index) throws RemoteException { + binder.addQueueItemWithIndex(controllerId, createBundleWithParcelable(description), index); + } + + public void removeQueueItem(MediaDescriptionCompat description) throws RemoteException { + binder.removeQueueItem(controllerId, createBundleWithParcelable(description)); + } + + public void setVolumeTo(int value, int flags) throws RemoteException { + binder.setVolumeTo(controllerId, value, flags); + } + + public void adjustVolume(int direction, int flags) throws RemoteException { + binder.adjustVolume(controllerId, direction, flags); + } + + public void sendCommand(String command, Bundle params, ResultReceiver cb) throws RemoteException { + binder.sendCommand(controllerId, command, params, cb); + } + + //////////////////////////////////////////////////////////////////////////////// + // MediaControllerCompat.TransportControls methods + //////////////////////////////////////////////////////////////////////////////// + + /** Transport controls */ + public class TransportControls { + public void prepare() throws RemoteException { + binder.prepare(controllerId); + } + + public void prepareFromMediaId(String mediaId, Bundle extras) throws RemoteException { + binder.prepareFromMediaId(controllerId, mediaId, extras); + } + + public void prepareFromSearch(String query, Bundle extras) throws RemoteException { + binder.prepareFromSearch(controllerId, query, extras); + } + + public void prepareFromUri(Uri uri, Bundle extras) throws RemoteException { + binder.prepareFromUri(controllerId, uri, extras); + } + + public void play() throws RemoteException { + binder.play(controllerId); + } + + public void playFromMediaId(String mediaId, Bundle extras) throws RemoteException { + binder.playFromMediaId(controllerId, mediaId, extras); + } + + public void playFromSearch(String query, Bundle extras) throws RemoteException { + binder.playFromSearch(controllerId, query, extras); + } + + public void playFromUri(Uri uri, Bundle extras) throws RemoteException { + binder.playFromUri(controllerId, uri, extras); + } + + public void skipToQueueItem(long id) throws RemoteException { + binder.skipToQueueItem(controllerId, id); + } + + public void pause() throws RemoteException { + binder.pause(controllerId); + } + + public void stop() throws RemoteException { + binder.stop(controllerId); + } + + public void seekTo(long pos) throws RemoteException { + binder.seekTo(controllerId, pos); + } + + public void setPlaybackSpeed(float speed) throws RemoteException { + binder.setPlaybackSpeed(controllerId, speed); + } + + public void skipToNext() throws RemoteException { + binder.skipToNext(controllerId); + } + + public void skipToPrevious() throws RemoteException { + binder.skipToPrevious(controllerId); + } + + public void setRating(RatingCompat rating) throws RemoteException { + binder.setRating(controllerId, createBundleWithParcelable(rating)); + } + + public void setRating(RatingCompat rating, Bundle extras) throws RemoteException { + binder.setRatingWithExtras(controllerId, createBundleWithParcelable(rating), extras); + } + + public void setCaptioningEnabled(boolean enabled) throws RemoteException { + binder.setCaptioningEnabled(controllerId, enabled); + } + + public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) + throws RemoteException { + binder.setRepeatMode(controllerId, repeatMode); + } + + public void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) + throws RemoteException { + binder.setShuffleMode(controllerId, shuffleMode); + } + + public void sendCustomAction(PlaybackStateCompat.CustomAction customAction, Bundle args) + throws RemoteException { + binder.sendCustomAction(controllerId, createBundleWithParcelable(customAction), args); + } + + public void sendCustomAction(String action, Bundle args) throws RemoteException { + binder.sendCustomActionWithName(controllerId, action, args); + } + } + + //////////////////////////////////////////////////////////////////////////////// + // Non-public methods + //////////////////////////////////////////////////////////////////////////////// + + /** + * Connects to client app's MediaControllerCompatProviderService. Should NOT be called main + * thread. + * + * @return true if connected successfully, false if failed to connect. + */ + private boolean connect() { + Intent intent = new Intent(ACTION_MEDIA_CONTROLLER_COMPAT); + intent.setComponent(MEDIA_CONTROLLER_COMPAT_PROVIDER_SERVICE); + + boolean bound = false; + try { + bound = context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); + } catch (Exception e) { + Log.e(TAG, "Failed to bind to the MediaControllerCompatProviderService.", e); + } + + if (bound) { + try { + countDownLatch.await(SERVICE_CONNECTION_TIMEOUT_MS, MILLISECONDS); + } catch (InterruptedException e) { + Log.e(TAG, "InterruptedException while waiting for onServiceConnected.", e); + } + } + return binder != null; + } + + /** Disconnects from client app's MediaControllerCompatProviderService. */ + private void disconnect() { + if (serviceConnection != null) { + context.unbindService(serviceConnection); + serviceConnection = null; + } + } + + /** + * Create a {@link MediaControllerCompat} in the client app. Should be used after successful + * connection through {@link #connect()}. + * + * @param waitForConnection true if this method needs to wait for the connection, false otherwise. + */ + void create(MediaSessionCompat.Token token, boolean waitForConnection) throws RemoteException { + binder.create(controllerId, createBundleWithParcelable(token), waitForConnection); + transportControls = new TransportControls(); + } + + private Bundle createBundleWithParcelable(Parcelable parcelable) { + Bundle bundle = new Bundle(); + bundle.putParcelable(KEY_ARGUMENTS, parcelable); + return bundle; + } + + class MyServiceConnection implements ServiceConnection { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + Log.d(TAG, "Connected to client app's MediaControllerCompatProviderService."); + binder = IRemoteMediaControllerCompat.Stub.asInterface(service); + countDownLatch.countDown(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + Log.d(TAG, "Disconnected from client app's MediaControllerCompatProviderService."); + } + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/RemoteMediaSession.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/RemoteMediaSession.java new file mode 100644 index 0000000000..7dc17803cd --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/RemoteMediaSession.java @@ -0,0 +1,617 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.ACTION_MEDIA2_SESSION; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_AUDIO_ATTRIBUTES; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_BUFFERED_PERCENTAGE; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_BUFFERED_POSITION; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_CONTENT_BUFFERED_POSITION; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_CONTENT_DURATION; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_CONTENT_POSITION; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_CURRENT_AD_GROUP_INDEX; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_CURRENT_AD_INDEX_IN_AD_GROUP; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_CURRENT_LIVE_OFFSET; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_CURRENT_PERIOD_INDEX; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_CURRENT_POSITION; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_CURRENT_WINDOW_INDEX; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_DEVICE_INFO; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_DEVICE_MUTED; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_DEVICE_VOLUME; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_DURATION; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_IS_LOADING; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_IS_PLAYING; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_IS_PLAYING_AD; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_MEDIA_ITEM; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_PLAYBACK_PARAMETERS; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_PLAYBACK_STATE; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_PLAYBACK_SUPPRESSION_REASON; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_PLAYER_ERROR; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_PLAYLIST_METADATA; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_PLAY_WHEN_READY; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_REPEAT_MODE; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_SHUFFLE_MODE_ENABLED; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_TIMELINE; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_TOTAL_BUFFERED_DURATION; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_VIDEO_SIZE; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_VOLUME; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.MEDIA2_SESSION_PROVIDER_SERVICE; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.SERVICE_CONNECTION_TIMEOUT_MS; +import static com.google.common.truth.Truth.assertWithMessage; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.support.v4.media.session.MediaSessionCompat; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaMetadata; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.PositionInfo; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.audio.AudioAttributes; +import com.google.android.exoplayer2.device.DeviceInfo; +import com.google.android.exoplayer2.session.vct.common.IRemoteMediaSession; +import com.google.android.exoplayer2.session.vct.common.TestUtils; +import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.video.VideoSize; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +/** + * Represents remote {@link MediaSession} in the service app's MediaSessionProviderService. Users + * can run {@link MediaSession} methods remotely with this object. + */ +public class RemoteMediaSession { + private static final String TAG = "RemoteMediaSession"; + + private final Context context; + private final String sessionId; + private final Bundle tokenExtras; + + private ServiceConnection serviceConnection; + private IRemoteMediaSession binder; + private RemoteMockPlayer remotePlayer; + private CountDownLatch countDownLatch; + + /** Create a {@link MediaSession} in the service app. Should NOT be called in main thread. */ + public RemoteMediaSession( + @NonNull String sessionId, @NonNull Context context, @Nullable Bundle tokenExtras) + throws RemoteException { + this.sessionId = sessionId; + this.context = context; + countDownLatch = new CountDownLatch(1); + serviceConnection = new MyServiceConnection(); + this.tokenExtras = tokenExtras; + + if (!connect()) { + assertWithMessage("Failed to connect to the MediaSessionProviderService.").fail(); + } + create(); + } + + public void cleanUp() throws RemoteException { + release(); + disconnect(); + } + + /** + * Gets {@link RemoteMockPlayer} for interact with the remote MockPlayer. Users can run MockPlayer + * methods remotely with this object. + */ + public RemoteMockPlayer getMockPlayer() { + return remotePlayer; + } + + //////////////////////////////////////////////////////////////////////////////// + // MediaSession methods + //////////////////////////////////////////////////////////////////////////////// + + /** + * Gets {@link SessionToken} from the service app. Should be used after the creation of the + * session through {@link #create()}. + * + * @return A {@link SessionToken} object. + */ + @Nullable + public SessionToken getToken() throws RemoteException { + return SessionToken.CREATOR.fromBundle(binder.getToken(sessionId)); + } + + /** + * Gets {@link MediaSessionCompat.Token} from the service app. Should be used after the creation + * of the session through {@link #create()}. + * + * @return A {@link SessionToken} object. + */ + @Nullable + public MediaSessionCompat.Token getCompatToken() throws RemoteException { + Bundle bundle = binder.getCompatToken(sessionId); + bundle.setClassLoader(MediaSession.class.getClassLoader()); + return MediaSessionCompat.Token.fromBundle(bundle); + } + + public void setSessionPositionUpdateDelayMs(long updateDelayMs) throws RemoteException { + binder.setSessionPositionUpdateDelayMs(sessionId, updateDelayMs); + } + + public void setPlayer(@NonNull Bundle config) throws RemoteException { + binder.setPlayer(sessionId, config); + } + + public void broadcastCustomCommand(@NonNull SessionCommand command, @Nullable Bundle args) + throws RemoteException { + binder.broadcastCustomCommand(sessionId, command.toBundle(), args); + } + + public void sendCustomCommand(@NonNull SessionCommand command, @Nullable Bundle args) + throws RemoteException { + binder.sendCustomCommand(sessionId, null, command.toBundle(), args); + } + + public void release() throws RemoteException { + binder.release(sessionId); + } + + public void setAvailableCommands( + @NonNull SessionCommands sessionCommands, @NonNull Player.Commands playerCommands) + throws RemoteException { + binder.setAvailableCommands( + sessionId, null, sessionCommands.toBundle(), playerCommands.toBundle()); + } + + public void setCustomLayout(@NonNull List layout) throws RemoteException { + List bundleList = new ArrayList<>(); + for (CommandButton button : layout) { + bundleList.add(button.toBundle()); + } + binder.setCustomLayout(sessionId, null, bundleList); + } + + //////////////////////////////////////////////////////////////////////////////// + // RemoteMockPlayer methods + //////////////////////////////////////////////////////////////////////////////// + + /** RemoteMockPlayer */ + public class RemoteMockPlayer { + + public void notifyPlayerError(@Nullable ExoPlaybackException playerError) + throws RemoteException { + binder.notifyPlayerError(sessionId, BundleableUtils.toNullableBundle(playerError)); + } + + public void setPlayWhenReady( + boolean playWhenReady, @Player.PlaybackSuppressionReason int reason) + throws RemoteException { + binder.setPlayWhenReady(sessionId, playWhenReady, reason); + } + + public void setPlaybackState(@Player.State int state) throws RemoteException { + binder.setPlaybackState(sessionId, state); + } + + public void setCurrentPosition(long pos) throws RemoteException { + binder.setCurrentPosition(sessionId, pos); + } + + public void setBufferedPosition(long pos) throws RemoteException { + binder.setBufferedPosition(sessionId, pos); + } + + public void setDuration(long duration) throws RemoteException { + binder.setDuration(sessionId, duration); + } + + public void setBufferedPercentage(int bufferedPercentage) throws RemoteException { + binder.setBufferedPercentage(sessionId, bufferedPercentage); + } + + public void setTotalBufferedDuration(long totalBufferedDuration) throws RemoteException { + binder.setTotalBufferedDuration(sessionId, totalBufferedDuration); + } + + public void setCurrentLiveOffset(long currentLiveOffset) throws RemoteException { + binder.setCurrentLiveOffset(sessionId, currentLiveOffset); + } + + public void setContentDuration(long contentDuration) throws RemoteException { + binder.setContentDuration(sessionId, contentDuration); + } + + public void setContentPosition(long contentPosition) throws RemoteException { + binder.setContentPosition(sessionId, contentPosition); + } + + public void setContentBufferedPosition(long contentBufferedPosition) throws RemoteException { + binder.setContentBufferedPosition(sessionId, contentBufferedPosition); + } + + public void setPlaybackParameters(PlaybackParameters playbackParameters) + throws RemoteException { + binder.setPlaybackParameters(sessionId, playbackParameters.toBundle()); + } + + public void setIsPlayingAd(boolean isPlayingAd) throws RemoteException { + binder.setIsPlayingAd(sessionId, isPlayingAd); + } + + public void setCurrentAdGroupIndex(int currentAdGroupIndex) throws RemoteException { + binder.setCurrentAdGroupIndex(sessionId, currentAdGroupIndex); + } + + public void setCurrentAdIndexInAdGroup(int currentAdIndexInAdGroup) throws RemoteException { + binder.setCurrentAdIndexInAdGroup(sessionId, currentAdIndexInAdGroup); + } + + public void notifyPlayWhenReadyChanged( + boolean playWhenReady, @Player.PlaybackSuppressionReason int reason) + throws RemoteException { + binder.notifyPlayWhenReadyChanged(sessionId, playWhenReady, reason); + } + + public void notifyPlaybackStateChanged(@Player.State int state) throws RemoteException { + binder.notifyPlaybackStateChanged(sessionId, state); + } + + public void notifyIsPlayingChanged(boolean isPlaying) throws RemoteException { + binder.notifyIsPlayingChanged(sessionId, isPlaying); + } + + public void notifyIsLoadingChanged(boolean isLoading) throws RemoteException { + binder.notifyIsLoadingChanged(sessionId, isLoading); + } + + public void notifyPositionDiscontinuity( + PositionInfo oldPosition, PositionInfo newPosition, @DiscontinuityReason int reason) + throws RemoteException { + binder.notifyPositionDiscontinuity( + sessionId, oldPosition.toBundle(), newPosition.toBundle(), reason); + } + + public void notifyPlaybackParametersChanged(PlaybackParameters playbackParameters) + throws RemoteException { + binder.notifyPlaybackParametersChanged(sessionId, playbackParameters.toBundle()); + } + + public void notifyMediaItemTransition(int index, @Player.MediaItemTransitionReason int reason) + throws RemoteException { + binder.notifyMediaItemTransition(sessionId, index, reason); + } + + public void notifyAudioAttributesChanged(@NonNull AudioAttributes audioAttributes) + throws RemoteException { + binder.notifyAudioAttributesChanged(sessionId, audioAttributes.toBundle()); + } + + public void notifyAvailableCommandsChanged(Player.Commands commands) throws RemoteException { + binder.notifyAvailableCommandsChanged(sessionId, commands.toBundle()); + } + + public void setTimeline(Timeline timeline) throws RemoteException { + binder.setTimeline(sessionId, timeline.toBundle()); + } + + /** + * Service app will automatically create a timeline of size {@code windowCount}, and sets it to + * the player. + * + *

Each item's media ID will be {@link TestUtils#getMediaIdInFakeTimeline(int)}. + */ + public void createAndSetFakeTimeline(int windowCount) throws RemoteException { + binder.createAndSetFakeTimeline(sessionId, windowCount); + } + + public void setPlaylistMetadata(MediaMetadata playlistMetadata) throws RemoteException { + binder.setPlaylistMetadata(sessionId, playlistMetadata.toBundle()); + } + + public void setRepeatMode(@Player.RepeatMode int repeatMode) throws RemoteException { + binder.setRepeatMode(sessionId, repeatMode); + } + + public void setShuffleModeEnabled(boolean shuffleModeEnabled) throws RemoteException { + binder.setShuffleModeEnabled(sessionId, shuffleModeEnabled); + } + + public void setCurrentWindowIndex(int index) throws RemoteException { + binder.setCurrentWindowIndex(sessionId, index); + } + + public void notifyTimelineChanged(@Player.TimelineChangeReason int reason) + throws RemoteException { + binder.notifyTimelineChanged(sessionId, reason); + } + + public void notifyPlaylistMetadataChanged() throws RemoteException { + binder.notifyPlaylistMetadataChanged(sessionId); + } + + public void notifyShuffleModeEnabledChanged() throws RemoteException { + binder.notifyShuffleModeEnabledChanged(sessionId); + } + + public void notifyRepeatModeChanged() throws RemoteException { + binder.notifyRepeatModeChanged(sessionId); + } + + public void notifyVideoSizeChanged(@NonNull VideoSize videoSize) throws RemoteException { + binder.notifyVideoSizeChanged(sessionId, videoSize.toBundle()); + } + + public boolean surfaceExists() throws RemoteException { + return binder.surfaceExists(sessionId); + } + + public void notifyDeviceVolumeChanged(int volume, boolean muted) throws RemoteException { + binder.notifyDeviceVolumeChanged(sessionId, volume, muted); + } + + public void notifyDeviceInfoChanged(@NonNull DeviceInfo deviceInfo) throws RemoteException { + binder.notifyDeviceInfoChanged(sessionId, deviceInfo.toBundle()); + } + } + + //////////////////////////////////////////////////////////////////////////////// + // Non-public methods + //////////////////////////////////////////////////////////////////////////////// + + /** + * Connects to service app's MediaSessionProviderService. Should NOT be called in main thread. + * + * @return true if connected successfully, false if failed to connect. + */ + private boolean connect() { + Intent intent = new Intent(ACTION_MEDIA2_SESSION); + intent.setComponent(MEDIA2_SESSION_PROVIDER_SERVICE); + + boolean bound = false; + try { + bound = context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); + } catch (RuntimeException e) { + Log.e(TAG, "Failed binding to the MediaSessionProviderService of the service app", e); + } + + if (bound) { + try { + countDownLatch.await(SERVICE_CONNECTION_TIMEOUT_MS, MILLISECONDS); + } catch (InterruptedException e) { + Log.e(TAG, "InterruptedException while waiting for onServiceConnected.", e); + } + } + return binder != null; + } + + /** Disconnects from service app's MediaSessionProviderService. */ + private void disconnect() { + if (serviceConnection != null) { + context.unbindService(serviceConnection); + } + serviceConnection = null; + } + + /** + * Create a {@link MediaSession} in the service app. Should be used after successful connection + * through {@link #connect}. + */ + private void create() throws RemoteException { + binder.create(sessionId, tokenExtras); + remotePlayer = new RemoteMockPlayer(); + } + + class MyServiceConnection implements ServiceConnection { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + Log.d(TAG, "Connected to service app's MediaSessionProviderService."); + binder = IRemoteMediaSession.Stub.asInterface(service); + countDownLatch.countDown(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + Log.d(TAG, "Disconnected from the service."); + } + } + + /** + * Builder to build a {@link Bundle} which represents a configuration of {@link SessionPlayer} in + * order to create a new mock player in the service app. The bundle can be passed to {@link + * #setPlayer(Bundle)}. + */ + public static final class MockPlayerConfigBuilder { + + private final Bundle bundle; + + public MockPlayerConfigBuilder() { + bundle = new Bundle(); + } + + public MockPlayerConfigBuilder setPlayerError(@Nullable ExoPlaybackException playerError) { + bundle.putBundle(KEY_PLAYER_ERROR, BundleableUtils.toNullableBundle(playerError)); + return this; + } + + public MockPlayerConfigBuilder setDuration(long duration) { + bundle.putLong(KEY_DURATION, duration); + return this; + } + + public MockPlayerConfigBuilder setCurrentPosition(long pos) { + bundle.putLong(KEY_CURRENT_POSITION, pos); + return this; + } + + public MockPlayerConfigBuilder setBufferedPosition(long buffPos) { + bundle.putLong(KEY_BUFFERED_POSITION, buffPos); + return this; + } + + public MockPlayerConfigBuilder setBufferedPercentage(int bufferedPercentage) { + bundle.putInt(KEY_BUFFERED_PERCENTAGE, bufferedPercentage); + return this; + } + + public MockPlayerConfigBuilder setTotalBufferedDuration(long totalBufferedDuration) { + bundle.putLong(KEY_TOTAL_BUFFERED_DURATION, totalBufferedDuration); + return this; + } + + public MockPlayerConfigBuilder setCurrentLiveOffset(long currentLiveOffset) { + bundle.putLong(KEY_CURRENT_LIVE_OFFSET, currentLiveOffset); + return this; + } + + public MockPlayerConfigBuilder setContentDuration(long contentDuration) { + bundle.putLong(KEY_CONTENT_DURATION, contentDuration); + return this; + } + + public MockPlayerConfigBuilder setContentPosition(long contentPosition) { + bundle.putLong(KEY_CONTENT_POSITION, contentPosition); + return this; + } + + public MockPlayerConfigBuilder setContentBufferedPosition(long contentBufferedPosition) { + bundle.putLong(KEY_CONTENT_BUFFERED_POSITION, contentBufferedPosition); + return this; + } + + public MockPlayerConfigBuilder setIsPlayingAd(boolean isPlayingAd) { + bundle.putBoolean(KEY_IS_PLAYING_AD, isPlayingAd); + return this; + } + + public MockPlayerConfigBuilder setCurrentAdGroupIndex(int currentAdGroupIndex) { + bundle.putInt(KEY_CURRENT_AD_GROUP_INDEX, currentAdGroupIndex); + return this; + } + + public MockPlayerConfigBuilder setCurrentAdIndexInAdGroup(int currentAdIndexInAdGroup) { + bundle.putInt(KEY_CURRENT_AD_INDEX_IN_AD_GROUP, currentAdIndexInAdGroup); + return this; + } + + public MockPlayerConfigBuilder setPlaybackParameters(PlaybackParameters playbackParameters) { + bundle.putBundle(KEY_PLAYBACK_PARAMETERS, playbackParameters.toBundle()); + return this; + } + + public MockPlayerConfigBuilder setAudioAttributes(@NonNull AudioAttributes audioAttributes) { + bundle.putBundle(KEY_AUDIO_ATTRIBUTES, audioAttributes.toBundle()); + return this; + } + + public MockPlayerConfigBuilder setTimeline(@NonNull Timeline timeline) { + bundle.putBundle(KEY_TIMELINE, timeline.toBundle()); + return this; + } + + public MockPlayerConfigBuilder setCurrentWindowIndex(int index) { + bundle.putInt(KEY_CURRENT_WINDOW_INDEX, index); + return this; + } + + public MockPlayerConfigBuilder setCurrentPeriodIndex(int index) { + bundle.putInt(KEY_CURRENT_PERIOD_INDEX, index); + return this; + } + + public MockPlayerConfigBuilder setPlaylistMetadata(MediaMetadata playlistMetadata) { + bundle.putBundle(KEY_PLAYLIST_METADATA, playlistMetadata.toBundle()); + return this; + } + + public MockPlayerConfigBuilder setCurrentMediaItem(@Nullable MediaItem item) { + bundle.putBundle(KEY_MEDIA_ITEM, BundleableUtils.toNullableBundle(item)); + return this; + } + + public MockPlayerConfigBuilder setVideoSize(@NonNull VideoSize videoSize) { + bundle.putBundle(KEY_VIDEO_SIZE, videoSize.toBundle()); + return this; + } + + public MockPlayerConfigBuilder setVolume(float volume) { + bundle.putFloat(KEY_VOLUME, volume); + return this; + } + + public MockPlayerConfigBuilder setDeviceInfo(@NonNull DeviceInfo deviceInfo) { + bundle.putBundle(KEY_DEVICE_INFO, deviceInfo.toBundle()); + return this; + } + + public MockPlayerConfigBuilder setDeviceVolume(int volume) { + bundle.putInt(KEY_DEVICE_VOLUME, volume); + return this; + } + + public MockPlayerConfigBuilder setDeviceMuted(boolean muted) { + bundle.putBoolean(KEY_DEVICE_MUTED, muted); + return this; + } + + public MockPlayerConfigBuilder setPlayWhenReady(boolean playWhenReady) { + bundle.putBoolean(KEY_PLAY_WHEN_READY, playWhenReady); + return this; + } + + public MockPlayerConfigBuilder setPlaybackSuppressionReason( + @Player.PlaybackSuppressionReason int playbackSuppressionReason) { + bundle.putInt(KEY_PLAYBACK_SUPPRESSION_REASON, playbackSuppressionReason); + return this; + } + + public MockPlayerConfigBuilder setPlaybackState(@Player.State int state) { + bundle.putInt(KEY_PLAYBACK_STATE, state); + return this; + } + + public MockPlayerConfigBuilder setIsPlaying(boolean isPlaying) { + bundle.putBoolean(KEY_IS_PLAYING, isPlaying); + return this; + } + + public MockPlayerConfigBuilder setIsLoading(boolean isLoading) { + bundle.putBoolean(KEY_IS_LOADING, isLoading); + return this; + } + + public MockPlayerConfigBuilder setRepeatMode(@Player.RepeatMode int repeatMode) { + bundle.putInt(KEY_REPEAT_MODE, repeatMode); + return this; + } + + public MockPlayerConfigBuilder setShuffleModeEnabled(boolean shuffleModeEnabled) { + bundle.putBoolean(KEY_SHUFFLE_MODE_ENABLED, shuffleModeEnabled); + return this; + } + + public Bundle build() { + return bundle; + } + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/RemoteMediaSessionCompat.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/RemoteMediaSessionCompat.java new file mode 100644 index 0000000000..2f583090f9 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/RemoteMediaSessionCompat.java @@ -0,0 +1,242 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.ACTION_MEDIA_SESSION_COMPAT; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_METADATA_COMPAT; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_PLAYBACK_STATE_COMPAT; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_QUEUE; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.KEY_SESSION_COMPAT_TOKEN; +import static com.google.android.exoplayer2.session.vct.common.CommonConstants.MEDIA_SESSION_COMPAT_PROVIDER_SERVICE; +import static com.google.android.exoplayer2.session.vct.common.TestUtils.SERVICE_CONNECTION_TIMEOUT_MS; +import static com.google.common.truth.Truth.assertWithMessage; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Parcelable; +import android.os.RemoteException; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.MediaSessionCompat.QueueItem; +import android.support.v4.media.session.PlaybackStateCompat; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.session.vct.common.IRemoteMediaSessionCompat; +import com.google.android.exoplayer2.util.Log; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +/** + * Represents remote {@link MediaSessionCompat} in the service app's + * MediaSessionCompatProviderService. Users can run {@link MediaSessionCompat} methods remotely with + * this object. + */ +public class RemoteMediaSessionCompat { + private static final String TAG = "RemoteMediaSessionCompat"; + + private final Context context; + private final String sessionTag; + + private ServiceConnection serviceConnection; + private IRemoteMediaSessionCompat binder; + private final CountDownLatch countDownLatch; + + /** + * Create a {@link MediaSessionCompat} in the service app. Should NOT be called in main thread. + */ + public RemoteMediaSessionCompat(@NonNull String sessionTag, Context context) + throws RemoteException { + this.sessionTag = sessionTag; + this.context = context; + countDownLatch = new CountDownLatch(1); + serviceConnection = new MyServiceConnection(); + + if (!connect()) { + assertWithMessage("Failed to connect to the MediaSessionCompatProviderService.").fail(); + } + create(); + } + + public void cleanUp() throws RemoteException { + release(); + disconnect(); + } + + //////////////////////////////////////////////////////////////////////////////// + // MediaSessionCompat methods + //////////////////////////////////////////////////////////////////////////////// + + /** + * Gets {@link MediaSessionCompat.Token} from the service app. Should be used after the creation + * of the session through {@link #create()}. + * + * @return A {@link MediaSessionCompat.Token} object if succeeded, {@code null} if failed. + */ + public MediaSessionCompat.Token getSessionToken() throws RemoteException { + MediaSessionCompat.Token token = null; + Bundle bundle = binder.getSessionToken(sessionTag); + if (bundle != null) { + bundle.setClassLoader(MediaSessionCompat.class.getClassLoader()); + token = bundle.getParcelable(KEY_SESSION_COMPAT_TOKEN); + } + return token; + } + + public void release() throws RemoteException { + binder.release(sessionTag); + } + + public void setPlaybackToLocal(int stream) throws RemoteException { + binder.setPlaybackToLocal(sessionTag, stream); + } + + /** + * Since we cannot pass VolumeProviderCompat directly, we pass volumeControl, maxVolume, + * currentVolume instead. + */ + public void setPlaybackToRemote(int volumeControl, int maxVolume, int currentVolume) + throws RemoteException { + binder.setPlaybackToRemote(sessionTag, volumeControl, maxVolume, currentVolume); + } + + public void setPlaybackState(PlaybackStateCompat state) throws RemoteException { + binder.setPlaybackState( + sessionTag, createBundleWithParcelable(KEY_PLAYBACK_STATE_COMPAT, state)); + } + + public void setMetadata(MediaMetadataCompat metadata) throws RemoteException { + binder.setMetadata(sessionTag, createBundleWithParcelable(KEY_METADATA_COMPAT, metadata)); + } + + public void setQueue(@Nullable List queue) throws RemoteException { + if (queue == null) { + binder.setQueue(sessionTag, null); + } else { + Bundle bundle = new Bundle(); + ArrayList queueAsArrayList = new ArrayList<>(queue); + bundle.putParcelableArrayList(KEY_QUEUE, queueAsArrayList); + binder.setQueue(sessionTag, bundle); + } + } + + public void setQueueTitle(CharSequence title) throws RemoteException { + binder.setQueueTitle(sessionTag, title); + } + + public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) throws RemoteException { + binder.setRepeatMode(sessionTag, repeatMode); + } + + public void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) + throws RemoteException { + binder.setShuffleMode(sessionTag, shuffleMode); + } + + public void setSessionActivity(PendingIntent intent) throws RemoteException { + binder.setSessionActivity(sessionTag, intent); + } + + public void setFlags(int flags) throws RemoteException { + binder.setFlags(sessionTag, flags); + } + + public void setRatingType(int type) throws RemoteException { + binder.setRatingType(sessionTag, type); + } + + public void sendSessionEvent(String event, Bundle extras) throws RemoteException { + binder.sendSessionEvent(sessionTag, event, extras); + } + + public void setCaptioningEnabled(boolean enabled) throws RemoteException { + binder.setCaptioningEnabled(sessionTag, enabled); + } + + //////////////////////////////////////////////////////////////////////////////// + // Non-public methods + //////////////////////////////////////////////////////////////////////////////// + + /** + * Connects to service app's MediaSessionCompatProviderService. Should NOT be called in main + * thread. + * + * @return true if connected successfully, false if failed to connect. + */ + private boolean connect() { + Intent intent = new Intent(ACTION_MEDIA_SESSION_COMPAT); + intent.setComponent(MEDIA_SESSION_COMPAT_PROVIDER_SERVICE); + + boolean bound = false; + try { + bound = context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); + } catch (RuntimeException e) { + Log.e(TAG, "Failed binding to the MediaSessionCompatProviderService of the service app", e); + } + + if (bound) { + try { + countDownLatch.await(SERVICE_CONNECTION_TIMEOUT_MS, MILLISECONDS); + } catch (InterruptedException e) { + Log.e(TAG, "InterruptedException while waiting for onServiceConnected.", e); + } + } + return binder != null; + } + + /** Disconnects from service app's MediaSessionCompatProviderService. */ + private void disconnect() { + if (serviceConnection != null) { + context.unbindService(serviceConnection); + } + serviceConnection = null; + } + + /** + * Create a {@link MediaSessionCompat} in the service app. Should be used after successful + * connection through {@link #connect}. + */ + private void create() throws RemoteException { + binder.create(sessionTag); + } + + private Bundle createBundleWithParcelable(String key, Parcelable value) { + Bundle bundle = new Bundle(); + bundle.putParcelable(key, value); + return bundle; + } + + class MyServiceConnection implements ServiceConnection { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + Log.d(TAG, "Connected to service app's MediaSessionCompatProviderService."); + binder = IRemoteMediaSessionCompat.Stub.asInterface(service); + countDownLatch.countDown(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + Log.d(TAG, "Disconnected from the service."); + } + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/TestBrowserCallback.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/TestBrowserCallback.java new file mode 100644 index 0000000000..e9da7a674c --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/TestBrowserCallback.java @@ -0,0 +1,123 @@ +/* + * Copyright 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. + */ + +package com.google.android.exoplayer2.session; + +import static com.google.android.exoplayer2.session.vct.common.TestUtils.SERVICE_CONNECTION_TIMEOUT_MS; +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.os.Bundle; +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.session.MediaBrowser.BrowserCallback; +import com.google.android.exoplayer2.session.MediaController.ControllerCallback; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +/** A proxy class for {@link BrowserCallback}. */ +public final class TestBrowserCallback implements BrowserCallback { + + private final ControllerCallback callbackProxy; + private final CountDownLatch connectLatch = new CountDownLatch(1); + private final CountDownLatch disconnectLatch = new CountDownLatch(1); + + @GuardedBy("this") + private Runnable onCustomCommandRunnable; + + public TestBrowserCallback(@Nullable ControllerCallback callbackProxy) { + this.callbackProxy = callbackProxy == null ? new BrowserCallback() {} : callbackProxy; + } + + @Override + public void onConnected(MediaController controller) { + connectLatch.countDown(); + callbackProxy.onConnected(controller); + } + + @Override + public void onDisconnected(@NonNull MediaController controller) { + disconnectLatch.countDown(); + callbackProxy.onDisconnected(controller); + } + + public void waitForConnect(boolean expected) throws InterruptedException { + if (expected) { + assertThat(connectLatch.await(SERVICE_CONNECTION_TIMEOUT_MS, MILLISECONDS)).isTrue(); + } else { + assertThat(connectLatch.await(SERVICE_CONNECTION_TIMEOUT_MS, MILLISECONDS)).isFalse(); + } + } + + public void waitForDisconnect(boolean expected) throws InterruptedException { + if (expected) { + assertThat(disconnectLatch.await(SERVICE_CONNECTION_TIMEOUT_MS, MILLISECONDS)).isTrue(); + } else { + assertThat(disconnectLatch.await(SERVICE_CONNECTION_TIMEOUT_MS, MILLISECONDS)).isFalse(); + } + } + + @Override + @NonNull + public ListenableFuture onCustomCommand( + @NonNull MediaController controller, @NonNull SessionCommand command, Bundle args) { + synchronized (this) { + if (onCustomCommandRunnable != null) { + onCustomCommandRunnable.run(); + } + } + return callbackProxy.onCustomCommand(controller, command, args); + } + + @Override + @NonNull + public ListenableFuture onSetCustomLayout( + @NonNull MediaController controller, @NonNull List layout) { + return callbackProxy.onSetCustomLayout(controller, layout); + } + + @Override + public void onAvailableSessionCommandsChanged( + MediaController controller, SessionCommands commands) { + callbackProxy.onAvailableSessionCommandsChanged(controller, commands); + } + + @Override + public void onChildrenChanged( + @NonNull MediaBrowser browser, + @NonNull String parentId, + int itemCount, + @Nullable MediaLibraryService.LibraryParams params) { + ((BrowserCallback) callbackProxy).onChildrenChanged(browser, parentId, itemCount, params); + } + + @Override + public void onSearchResultChanged( + @NonNull MediaBrowser browser, + @NonNull String query, + int itemCount, + @Nullable MediaLibraryService.LibraryParams params) { + ((BrowserCallback) callbackProxy).onSearchResultChanged(browser, query, itemCount, params); + } + + public void setRunnableForOnCustomCommand(Runnable runnable) { + synchronized (this) { + onCustomCommandRunnable = runnable; + } + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/TestServiceRegistry.java b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/TestServiceRegistry.java new file mode 100644 index 0000000000..ac86a522a7 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/test_session_current/src/main/java/com/google/android/exoplayer2/session/TestServiceRegistry.java @@ -0,0 +1,143 @@ +/* + * 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.google.android.exoplayer2.session; + +import static com.google.common.truth.Truth.assertWithMessage; + +import androidx.annotation.GuardedBy; +import com.google.android.exoplayer2.session.MediaLibraryService.MediaLibrarySession.MediaLibrarySessionCallback; +import com.google.android.exoplayer2.session.MediaSession.ControllerInfo; +import java.util.List; + +/** + * Keeps the instance of currently running {@link MockMediaSessionService}. And also provides a way + * to control them in one place. + * + *

It only support only one service at a time. + */ +public class TestServiceRegistry { + + @GuardedBy("TestServiceRegistry.class") + private static TestServiceRegistry instance; + + @GuardedBy("TestServiceRegistry.class") + private MediaSessionService service; + + @GuardedBy("TestServiceRegistry.class") + private MediaLibrarySessionCallback sessionCallback; + + @GuardedBy("TestServiceRegistry.class") + private SessionServiceCallback sessionServiceCallback; + + @GuardedBy("TestServiceRegistry.class") + private OnGetSessionHandler onGetSessionHandler; + + /** Callback for session service's lifecyle (onCreate() / onDestroy()) */ + public interface SessionServiceCallback { + void onCreated(); + + void onDestroyed(); + } + + public static TestServiceRegistry getInstance() { + synchronized (TestServiceRegistry.class) { + if (instance == null) { + instance = new TestServiceRegistry(); + } + return instance; + } + } + + public void setOnGetSessionHandler(OnGetSessionHandler onGetSessionHandler) { + synchronized (TestServiceRegistry.class) { + this.onGetSessionHandler = onGetSessionHandler; + } + } + + public OnGetSessionHandler getOnGetSessionHandler() { + synchronized (TestServiceRegistry.class) { + return onGetSessionHandler; + } + } + + public void setSessionServiceCallback(SessionServiceCallback sessionServiceCallback) { + synchronized (TestServiceRegistry.class) { + this.sessionServiceCallback = sessionServiceCallback; + } + } + + public void setSessionCallback(MediaLibrarySessionCallback sessionCallback) { + synchronized (TestServiceRegistry.class) { + this.sessionCallback = sessionCallback; + } + } + + public MediaLibrarySessionCallback getSessionCallback() { + synchronized (TestServiceRegistry.class) { + return sessionCallback; + } + } + + public void setServiceInstance(MediaSessionService service) { + synchronized (TestServiceRegistry.class) { + if (this.service != null) { + assertWithMessage( + "Previous service instance is still running. Clean up manually to ensure" + + " previously running service doesn't break current test") + .fail(); + } + this.service = service; + if (sessionServiceCallback != null) { + sessionServiceCallback.onCreated(); + } + } + } + + public MediaSessionService getServiceInstance() { + synchronized (TestServiceRegistry.class) { + return service; + } + } + + public void cleanUp() { + synchronized (TestServiceRegistry.class) { + if (service != null) { + // TODO(jaewan): Remove this, and override SessionService#onDestroy() to do this + List sessions = service.getSessions(); + for (int i = 0; i < sessions.size(); i++) { + sessions.get(i).release(); + } + // stopSelf() would not kill service while the binder connection established by + // bindService() exists, and release() above will do the job instead. + // So stopSelf() isn't really needed, but just for sure. + service.stopSelf(); + service = null; + } + sessionCallback = null; + if (sessionServiceCallback != null) { + sessionServiceCallback.onDestroyed(); + sessionServiceCallback = null; + } + onGetSessionHandler = null; + } + } + + /** Handles onGetSession */ + public interface OnGetSessionHandler { + MediaSession onGetSession(ControllerInfo controllerInfo); + } +}