3. 构建音频播放器状态机
任务说明
在Android音频应用开发中,播放器需要处理播放、暂停、停止、释放等多种操作,且不同操作在不同状态下行为不同。本案例实现一个轻量级音频播放器状态机,管理IDLE、INITIALIZED、PREPARED、STARTED、PAUSED、STOPPED、COMPLETED、ERROR、RELEASED九种状态,并约束状态迁移合法性。
状态机设计
状态机核心为AudioPlayerStateMachine类,内部维护当前状态与合法迁移表。对外暴露setDataSource()、prepare()、start()、pause()、stop()、seekTo()、release()、reset()、onCompletion()、onError()十个操作。每个操作根据当前状态决定是否允许迁移,非法操作抛出IllegalStateException并附带当前状态与操作说明。
核心实现
// AudioPlayerStateMachine.java
package com.example.audioplayer;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
public class AudioPlayerStateMachine {
public enum State {
IDLE, INITIALIZED, PREPARED, STARTED, PAUSED, STOPPED,
COMPLETED, ERROR, RELEASED
}
public enum Operation {
SET_DATA_SOURCE, PREPARE, START, PAUSE, STOP, SEEK_TO,
RELEASE, RESET, ON_COMPLETION, ON_ERROR
}
private State currentState = State.IDLE;
private final Map<State, EnumSet<Operation>> transitionTable = new HashMap<>();
public AudioPlayerStateMachine() {
transitionTable.put(State.IDLE,
EnumSet.of(Operation.SET_DATA_SOURCE, Operation.RELEASE, Operation.RESET));
transitionTable.put(State.INITIALIZED,
EnumSet.of(Operation.PREPARE, Operation.RELEASE, Operation.RESET));
transitionTable.put(State.PREPARED,
EnumSet.of(Operation.START, Operation.STOP, Operation.SEEK_TO,
Operation.RELEASE, Operation.RESET));
transitionTable.put(State.STARTED,
EnumSet.of(Operation.PAUSE, Operation.STOP, Operation.SEEK_TO,
Operation.RELEASE, Operation.RESET, Operation.ON_COMPLETION, Operation.ON_ERROR));
transitionTable.put(State.PAUSED,
EnumSet.of(Operation.START, Operation.STOP, Operation.SEEK_TO,
Operation.RELEASE, Operation.RESET, Operation.ON_ERROR));
transitionTable.put(State.STOPPED,
EnumSet.of(Operation.START, Operation.RELEASE, Operation.RESET));
transitionTable.put(State.COMPLETED,
EnumSet.of(Operation.START, Operation.STOP, Operation.RELEASE, Operation.RESET));
transitionTable.put(State.ERROR,
EnumSet.of(Operation.RELEASE, Operation.RESET));
transitionTable.put(State.RELEASED, EnumSet.noneOf(Operation.class));
}
public State getCurrentState() {
return currentState;
}
private void transition(Operation op) {
EnumSet<Operation> allowed = transitionTable.get(currentState);
if (allowed == null || !allowed.contains(op)) {
throw new IllegalStateException(
"非法操作: 当前状态=" + currentState + ", 操作=" + op);
}
switch (op) {
case SET_DATA_SOURCE: currentState = State.INITIALIZED; break;
case PREPARE: currentState = State.PREPARED; break;
case START:
if (currentState == State.PREPARED || currentState == State.PAUSED ||
currentState == State.STOPPED || currentState == State.COMPLETED) {
currentState = State.STARTED;
}
break;
case PAUSE: currentState = State.PAUSED; break;
case STOP: currentState = State.STOPPED; break;
case SEEK_TO: /* 保持当前状态不变 */ break;
case RELEASE: currentState = State.RELEASED; break;
case RESET: currentState = State.IDLE; break;
case ON_COMPLETION: currentState = State.COMPLETED; break;
case ON_ERROR: currentState = State.ERROR; break;
}
}
// 对外API
public void setDataSource() { transition(Operation.SET_DATA_SOURCE); }
public void prepare() { transition(Operation.PREPARE); }
public void start() { transition(Operation.START); }
public void pause() { transition(Operation.PAUSE); }
public void stop() { transition(Operation.STOP); }
public void seekTo() { transition(Operation.SEEK_TO); }
public void release() { transition(Operation.RELEASE); }
public void reset() { transition(Operation.RESET); }
public void onCompletion() { transition(Operation.ON_COMPLETION); }
public void onError() { transition(Operation.ON_ERROR); }
}
验证方法
编写单元测试验证状态迁移正确性与非法操作拦截能力。测试覆盖以下场景:
// AudioPlayerStateMachineTest.java
import static org.junit.Assert.*;
import org.junit.Test;
public class AudioPlayerStateMachineTest {
@Test
public void testNormalPlaybackFlow() {
AudioPlayerStateMachine sm = new AudioPlayerStateMachine();
assertEquals(AudioPlayerStateMachine.State.IDLE, sm.getCurrentState());
sm.setDataSource();
assertEquals(AudioPlayerStateMachine.State.INITIALIZED, sm.getCurrentState());
sm.prepare();
assertEquals(AudioPlayerStateMachine.State.PREPARED, sm.getCurrentState());
sm.start();
assertEquals(AudioPlayerStateMachine.State.STARTED, sm.getCurrentState());
sm.pause();
assertEquals(AudioPlayerStateMachine.State.PAUSED, sm.getCurrentState());
sm.start();
assertEquals(AudioPlayerStateMachine.State.STARTED, sm.getCurrentState());
sm.onCompletion();
assertEquals(AudioPlayerStateMachine.State.COMPLETED, sm.getCurrentState());
sm.stop();
assertEquals(AudioPlayerStateMachine.State.STOPPED, sm.getCurrentState());
sm.release();
assertEquals(AudioPlayerStateMachine.State.RELEASED, sm.getCurrentState());
}
@Test(expected = IllegalStateException.class)
public void testIllegalStartFromIdle() {
AudioPlayerStateMachine sm = new AudioPlayerStateMachine();
sm.start(); // IDLE状态不允许直接start
}
@Test(expected = IllegalStateException.class)
public void testIllegalPauseFromPrepared() {
AudioPlayerStateMachine sm = new AudioPlayerStateMachine();
sm.setDataSource();
sm.prepare();
sm.pause(); // PREPARED状态不允许pause
}
@Test
public void testErrorAndResetFlow() {
AudioPlayerStateMachine sm = new AudioPlayerStateMachine();
sm.setDataSource();
sm.prepare();
sm.start();
sm.onError();
assertEquals(AudioPlayerStateMachine.State.ERROR, sm.getCurrentState());
sm.reset();
assertEquals(AudioPlayerStateMachine.State.IDLE, sm.getCurrentState());
}
@Test
public void testSeekToKeepsState() {
AudioPlayerStateMachine sm = new AudioPlayerStateMachine();
sm.setDataSource();
sm.prepare();
sm.seekTo();
assertEquals(AudioPlayerStateMachine.State.PREPARED, sm.getCurrentState());
sm.start();
sm.seekTo();
assertEquals(AudioPlayerStateMachine.State.STARTED, sm.getCurrentState());
}
}
运行与判定标准
运行方式:将两个Java文件放入同一包目录,使用JUnit 4运行测试类。若使用Gradle,在build.gradle中添加testImplementation 'junit:junit:4.13.2'后执行gradle test;若使用Maven,添加JUnit依赖后执行mvn test。
判定标准:所有测试方法全部通过(绿色),即状态机在正常播放流程中按预期迁移,非法操作抛出IllegalStateException,错误恢复与重置路径正确,seekTo不改变当前状态。若任一测试失败,说明状态迁移表或状态更新逻辑存在缺陷,需对照状态图修正。