3. 构建音频播放器状态机

构建音频播放器状态机 学习卡片

任务说明

在Android音频应用开发中,播放器需要处理播放、暂停、停止、释放等多种操作,且不同操作在不同状态下行为不同。本案例实现一个轻量级音频播放器状态机,管理IDLE、INITIALIZED、PREPARED、STARTED、PAUSED、STOPPED、COMPLETED、ERROR、RELEASED九种状态,并约束状态迁移合法性。

状态机设计

状态机核心为AudioPlayerStateMachine类,内部维护当前状态与合法迁移表。对外暴露setDataSource()、prepare()、start()、pause()、stop()、seekTo()、release()、reset()、onCompletion()、onError()十个操作。每个操作根据当前状态决定是否允许迁移,非法操作抛出IllegalStateException并附带当前状态与操作说明。

IDLE INITIALIZED PREPARED STARTED PAUSED STOPPED COMPLETED ERROR RELEASED setDataSource prepare start pause stop start reset reset onError release release release release release release release onCompletion onError 正常迁移 错误路径 release路径 完成回调

核心实现


// 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不改变当前状态。若任一测试失败,说明状态迁移表或状态更新逻辑存在缺陷,需对照状态图修正。