Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,10 @@
* Implemented `onAudioSessionIdChanged` to notify media controllers when
an audio session ID is set by the session
([#244](https://github.com/androidx/media/issues/244)).
* Fix bug where `KEYCODE_HEADSETHOOK` did not start the player upon and
media key event `Intent` arriving in `onStartCommand()`. This is fixed
by handling 'KEYCODE_HEADSETHOOK' just like `KEYCODE_MEDIA_PLAY_PAUSE`
([#2816](https://github.com/androidx/media/pull/2816)).
* UI:
* Add `ProgressStateWithTickInterval` class and the corresponding
`rememberProgressStateWithTickInterval` Composable to
Expand Down
1 change: 1 addition & 0 deletions libraries/session/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ dependencies {
testImplementation project(modulePrefix + 'test-utils-robolectric')
testImplementation project(modulePrefix + 'lib-exoplayer')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
testImplementation 'com.google.testparameterinjector:test-parameter-injector:' + testParameterInjectorVersion
}

ext {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package androidx.media3.session;

import static android.view.KeyEvent.KEYCODE_HEADSETHOOK;
import static android.view.KeyEvent.KEYCODE_MEDIA_FAST_FORWARD;
import static android.view.KeyEvent.KEYCODE_MEDIA_NEXT;
import static android.view.KeyEvent.KEYCODE_MEDIA_PAUSE;
Expand Down Expand Up @@ -1431,6 +1432,7 @@ private void handleAvailablePlayerCommandsChanged(Player.Commands availableComma
if (keyEvent.getAction() != KeyEvent.ACTION_DOWN) {
switch (keyEvent.getKeyCode()) {
case KEYCODE_MEDIA_PLAY_PAUSE:
case KEYCODE_HEADSETHOOK:
case KEYCODE_MEDIA_PLAY:
case KEYCODE_MEDIA_PAUSE:
case KEYCODE_MEDIA_NEXT:
Expand All @@ -1453,8 +1455,8 @@ private void handleAvailablePlayerCommandsChanged(Player.Commands availableComma
boolean isTvApp = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
boolean doubleTapCompleted = false;
switch (keyCode) {
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
case KeyEvent.KEYCODE_HEADSETHOOK:
case KEYCODE_MEDIA_PLAY_PAUSE:
case KEYCODE_HEADSETHOOK:
if (isTvApp
|| callerInfo.getControllerVersion() != ControllerInfo.LEGACY_CONTROLLER_VERSION
|| keyEvent.getRepeatCount() != 0) {
Expand All @@ -1479,7 +1481,7 @@ private void handleAvailablePlayerCommandsChanged(Player.Commands availableComma
}

if (!isMediaNotificationControllerConnected()) {
if ((keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE || keyCode == KeyEvent.KEYCODE_HEADSETHOOK)
if ((keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE || keyCode == KEYCODE_HEADSETHOOK)
&& doubleTapCompleted) {
// Double tap completion for legacy when media notification controller is disabled.
sessionLegacyStub.onSkipToNext();
Expand All @@ -1505,12 +1507,13 @@ private boolean applyMediaButtonKeyEvent(
ControllerInfo controllerInfo = checkNotNull(instance.getMediaNotificationControllerInfo());
Runnable command;
int keyCode = keyEvent.getKeyCode();
if ((keyCode == KEYCODE_MEDIA_PLAY_PAUSE || keyCode == KeyEvent.KEYCODE_HEADSETHOOK)
if ((keyCode == KEYCODE_MEDIA_PLAY_PAUSE || keyCode == KEYCODE_HEADSETHOOK)
&& doubleTapCompleted) {
keyCode = KEYCODE_MEDIA_NEXT;
}
switch (keyCode) {
case KEYCODE_MEDIA_PLAY_PAUSE:
case KEYCODE_HEADSETHOOK:
command =
getPlayerWrapper().getPlayWhenReady()
? () -> sessionStub.pauseForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.test.utils.TestExoPlayerBuilder;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.testing.junit.testparameterinjector.TestParameter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
Expand All @@ -51,10 +51,11 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestParameterInjector;
import org.robolectric.android.controller.ServiceController;
import org.robolectric.shadows.ShadowLooper;

@RunWith(AndroidJUnit4.class)
@RunWith(RobolectricTestParameterInjector.class)
public class MediaSessionServiceTest {

private static final int TIMEOUT_MS = 500;
Expand Down Expand Up @@ -748,11 +749,12 @@ public void pause() {
}

@Test
public void onStartCommand_playbackResumption_calledByMediaNotificationController()
public void onStartCommand_playbackResumption_calledByMediaNotificationController(
@TestParameter PlayPauseEvent playPauseEvent)
throws InterruptedException, ExecutionException, TimeoutException {
Intent playIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
playIntent.putExtra(
Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY));
Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, playPauseEvent.keyCode));
ServiceController<TestServiceWithPlaybackResumption> serviceController =
Robolectric.buildService(TestServiceWithPlaybackResumption.class, playIntent);
TestServiceWithPlaybackResumption service = serviceController.create().get();
Expand Down Expand Up @@ -1011,4 +1013,16 @@ public void onDestroy() {
super.onDestroy();
}
}

private enum PlayPauseEvent {
MEDIA_PLAY_PAUSE(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE),
MEDIA_PLAY(KeyEvent.KEYCODE_MEDIA_PLAY),
HEADSETHOOK(KeyEvent.KEYCODE_HEADSETHOOK);

final int keyCode;

PlayPauseEvent(int keyCode) {
this.keyCode = keyCode;
}
}
}
1 change: 1 addition & 0 deletions libraries/test_session_current/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ dependencies {
implementation project(modulePrefix + 'test-data')
androidTestImplementation project(modulePrefix + 'lib-exoplayer')
androidTestImplementation project(modulePrefix + 'test-utils')
androidTestImplementation 'com.google.testparameterinjector:test-parameter-injector:' + testParameterInjectorVersion
androidTestImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion
androidTestImplementation 'androidx.test.ext:truth:' + androidxTestTruthVersion
androidTestImplementation 'androidx.test:core:' + androidxTestCoreVersion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@
import androidx.media3.test.session.common.R;
import androidx.media3.test.session.common.TestHandler;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import com.google.common.collect.ImmutableList;
import com.google.testing.junit.testparameterinjector.TestParameter;
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
Expand All @@ -59,7 +60,7 @@
* 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.
*/
@RunWith(AndroidJUnit4.class)
@RunWith(TestParameterInjector.class)
@LargeTest
public class MediaSessionKeyEventTest {

Expand Down Expand Up @@ -271,65 +272,80 @@ public void stopKeyEvent() throws Exception {
}

@Test
public void playPauseKeyEvent_paused_play() throws Exception {
public void playPauseKeyEvent_paused_play(@TestParameter PlayPauseEvent playPauseEvent)
throws Exception {
// We don't receive media key events when we are not playing on API < 26, so we can't test this
// case as it's not supported.
assumeTrue(SDK_INT >= 26);

handler.postAndSync(
() -> {
player.playbackState = Player.STATE_READY;
});

dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, false);
dispatchMediaKeyEvent(playPauseEvent.keyCode, /* doubleTap= */ false);

player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
}

@Test
public void playPauseKeyEvent_fromIdle_prepareAndPlay() throws Exception {
public void playPauseKeyEvent_fromIdle_prepareAndPlay(
@TestParameter PlayPauseEvent playPauseEvent) throws Exception {
// We don't receive media key events when we are not playing on API < 26, so we can't test this
// case as it's not supported.
assumeTrue(SDK_INT >= 26);

handler.postAndSync(
() -> {
player.playbackState = Player.STATE_IDLE;
});

dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, false);
dispatchMediaKeyEvent(playPauseEvent.keyCode, /* doubleTap= */ false);

player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS);
player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
}

@Test
public void playPauseKeyEvent_playWhenReadyAndEnded_seekAndPlay() throws Exception {
public void playPauseKeyEvent_playWhenReadyAndEnded_seekAndPlay(
@TestParameter PlayPauseEvent playPauseEvent) throws Exception {
// We don't receive media key events when we are not playing on API < 26, so we can't test this
// case as it's not supported.
assumeTrue(SDK_INT >= 26);

handler.postAndSync(
() -> {
player.playWhenReady = true;
player.playbackState = STATE_ENDED;
});

dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, false);
dispatchMediaKeyEvent(playPauseEvent.keyCode, /* doubleTap= */ false);

player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_DEFAULT_POSITION, TIMEOUT_MS);
player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
}

@Test
public void playPauseKeyEvent_playing_pause() throws Exception {
public void playPauseKeyEvent_playing_pause()
throws Exception {
handler.postAndSync(
() -> {
player.playWhenReady = true;
player.playbackState = Player.STATE_READY;
});

dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, /* doubleTap= */ false);

player.awaitMethodCalled(MockPlayer.METHOD_PAUSE, TIMEOUT_MS);
}

@Test
public void headsetHookKeyEvent_playing_pause()
throws Exception {
handler.postAndSync(
() -> {
player.playWhenReady = true;
player.playbackState = Player.STATE_READY;
});

dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, false);
dispatchMediaKeyEvent(KeyEvent.KEYCODE_HEADSETHOOK, /* doubleTap= */ false);

player.awaitMethodCalled(MockPlayer.METHOD_PAUSE, TIMEOUT_MS);
}
Expand All @@ -348,7 +364,7 @@ public void playPauseKeyEvent_doubleTapOnPlayPause_seekNext() throws Exception {
}

@Test
public void playPauseKeyEvent_doubleTapOnHeadsetHook_seekNext() throws Exception {
public void headsetHookKeyEvent_doubleTapOnPlayPause_seekNext() throws Exception {
handler.postAndSync(
() -> {
player.playWhenReady = true;
Expand Down Expand Up @@ -516,4 +532,16 @@ public void seekBack() {
super.seekBack();
}
}

private enum PlayPauseEvent {
MEDIA_PLAY_PAUSE(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE),
MEDIA_PLAY(KeyEvent.KEYCODE_MEDIA_PLAY),
HEADSETHOOK(KeyEvent.KEYCODE_HEADSETHOOK);

final int keyCode;

PlayPauseEvent(int keyCode) {
this.keyCode = keyCode;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package androidx.media3.session;

import static android.os.Build.VERSION.SDK_INT;
import static android.view.KeyEvent.KEYCODE_HEADSETHOOK;
import static android.view.KeyEvent.KEYCODE_MEDIA_FAST_FORWARD;
import static android.view.KeyEvent.KEYCODE_MEDIA_NEXT;
import static android.view.KeyEvent.KEYCODE_MEDIA_PAUSE;
Expand Down Expand Up @@ -634,6 +635,14 @@ controllerInfo, getMediaButtonIntent(KEYCODE_MEDIA_PREVIOUS)))
impl.onMediaButtonEvent(
controllerInfo, getMediaButtonIntent(KEYCODE_MEDIA_STOP)))
.isTrue();
assertThat(
impl.onMediaButtonEvent(
controllerInfo, getMediaButtonIntent(KEYCODE_MEDIA_PLAY_PAUSE)))
.isTrue();
assertThat(
impl.onMediaButtonEvent(
controllerInfo, getMediaButtonIntent(KEYCODE_HEADSETHOOK)))
.isTrue();
});

player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
Expand All @@ -643,7 +652,7 @@ controllerInfo, getMediaButtonIntent(KEYCODE_MEDIA_STOP)))
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_NEXT, TIMEOUT_MS);
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_PREVIOUS, TIMEOUT_MS);
player.awaitMethodCalled(MockPlayer.METHOD_STOP, TIMEOUT_MS);
assertThat(callerCollectorPlayer.callingControllers).hasSize(7);
assertThat(callerCollectorPlayer.callingControllers).hasSize(9);
for (ControllerInfo controllerInfo : callerCollectorPlayer.callingControllers) {
assertThat(session.get().isMediaNotificationController(controllerInfo)).isTrue();
}
Expand Down