Android 9系统源码_音频管理(一)按键音效源码解析

前言

当用户点击Android智能设备的按钮的时候,如果伴随有按键音效的话,会给用户更好的交互体验。本期我们将会结合Android系统源码来具体分析一下控件是如何发出按键音效的。

一、系统加载按键音效资源

1、在TV版的Android智能设备中,我们可以通过调节设置页面的开关来控制按键音效的有无,该设置页面对应的系统源码如下所示。

packages/apps/TvSettings/Settings/src/com/android/tv/settings/device/sound/SoundFragment.java

java 复制代码
public class SoundFragment extends PreferenceControllerFragment implements
        Preference.OnPreferenceChangeListener {

    private AudioManager mAudioManager;
    private Map<Integer, Boolean> mFormats;

    public static SoundFragment newInstance() {
        return new SoundFragment();
    }

    @Override
    public void onAttach(Context context) {
        mAudioManager = context.getSystemService(AudioManager.class);
        mFormats = mAudioManager.getSurroundFormats();
        super.onAttach(context);
    }
    
	//用户的点击行为首先触发此方法
    @Override
    public boolean onPreferenceTreeClick(Preference preference) {
        if (TextUtils.equals(preference.getKey(), KEY_SOUND_EFFECTS)) {
            final TwoStatePreference soundPref = (TwoStatePreference) preference;
            //调用setSoundEffectsEnabled来设置按键音的开启与关闭
            setSoundEffectsEnabled(soundPref.isChecked());
        }
        return super.onPreferenceTreeClick(preference);
    }

	//获取按键音效是否开启
    public static boolean getSoundEffectsEnabled(ContentResolver contentResolver) {
        return Settings.System.getInt(contentResolver, Settings.System.SOUND_EFFECTS_ENABLED, 1)
                != 0;
    }

	//设置是否开启按键音效
    private void setSoundEffectsEnabled(boolean enabled) {
        if (enabled) {
        	//如果开启按键音,则调用AudioManager的loadSoundEffects方法来加载按键音效资源
            mAudioManager.loadSoundEffects();
        } else {
            mAudioManager.unloadSoundEffects();
        }
        Settings.System.putInt(getActivity().getContentResolver(),
                Settings.System.SOUND_EFFECTS_ENABLED, enabled ? 1 : 0);
    }

}

我们在设置页面点击按键音效开关按钮,最终会触发SoundFragment的setSoundEffectsEnabled方法,该方法会判断是否开启按键音,如果开启,则调用AudioManager的loadSoundEffects方法来加载按键音效资源,反之则会调用unloadSoundEffects方法不加载音效资源。

2、AudioManager的loadSoundEffects方法如下所示。

frameworks/base/media/java/android/media/AudioManager.java

java 复制代码
public class AudioManager {

    //获取AudioService的代理对象
    private static IAudioService getService()
    {
        if (sService != null) {
            return sService;
        }
        IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
        sService = IAudioService.Stub.asInterface(b);
        return sService;
    }

    public void loadSoundEffects() {
        final IAudioService service = getService();
        try {
        	//调用AudioService服务的loadSoundEffects方法
            service.loadSoundEffects();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }
    
    public void unloadSoundEffects() {
        final IAudioService service = getService();
        try {
            service.unloadSoundEffects();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }
}

AudioManager的首先通过getService方法获取了音频服务AudioService的代理对象,然后调用该对象的具体方法。

3、AudioService的loadSoundEffects方法如下所示。

java 复制代码
public class AudioService extends IAudioService.Stub
        implements AccessibilityManager.TouchExplorationStateChangeListener,
            AccessibilityManager.AccessibilityServicesStateChangeListener {
       
     private AudioHandler mAudioHandler;

	 //加载音效资源
     public boolean loadSoundEffects() {
        int attempts = 3;
        LoadSoundEffectReply reply = new LoadSoundEffectReply();

        synchronized (reply) {
        	//调用sendMsg发送消息给mAudioHandler。
            sendMsg(mAudioHandler, MSG_LOAD_SOUND_EFFECTS, SENDMSG_QUEUE, 0, 0, reply, 0);
            while ((reply.mStatus == 1) && (attempts-- > 0)) {
                try {
                    reply.wait(SOUND_EFFECTS_LOAD_TIMEOUT_MS);
                } catch (InterruptedException e) {
                    Log.w(TAG, "loadSoundEffects Interrupted while waiting sound pool loaded.");
                }
            }
        }
        return (reply.mStatus == 0);
    }
    
    //不加载音效资源
    public void unloadSoundEffects() {
        sendMsg(mAudioHandler, MSG_UNLOAD_SOUND_EFFECTS, SENDMSG_QUEUE, 0, 0, null, 0);
    }

    private static void sendMsg(Handler handler, int msg,
            int existingMsgPolicy, int arg1, int arg2, Object obj, int delay) {

        if (existingMsgPolicy == SENDMSG_REPLACE) {
            handler.removeMessages(msg);
        } else if (existingMsgPolicy == SENDMSG_NOOP && handler.hasMessages(msg)) {
            return;
        }
        synchronized (mLastDeviceConnectMsgTime) {
            long time = SystemClock.uptimeMillis() + delay;

            if (msg == MSG_SET_A2DP_SRC_CONNECTION_STATE ||
                msg == MSG_SET_A2DP_SINK_CONNECTION_STATE ||
                msg == MSG_SET_HEARING_AID_CONNECTION_STATE ||
                msg == MSG_SET_WIRED_DEVICE_CONNECTION_STATE ||
                msg == MSG_A2DP_DEVICE_CONFIG_CHANGE ||
                msg == MSG_BTA2DP_DOCK_TIMEOUT) {
                if (mLastDeviceConnectMsgTime >= time) {
                  // add a little delay to make sure messages are ordered as expected
                  time = mLastDeviceConnectMsgTime + 30;
                }
                mLastDeviceConnectMsgTime = time;
            }
			//调用handler的sendMessageAtTime方法
            handler.sendMessageAtTime(handler.obtainMessage(msg, arg1, arg2, obj), time);
        }
    }
    
    private class AudioHandler extends Handler {
    		//加载音效
            private boolean onLoadSoundEffects() {
            	...代码暂时省略...
            }
            @Override
        	public void handleMessage(Message msg) {
            switch (msg.what) {
            	...代码省略...
            	 case MSG_UNLOAD_SOUND_EFFECTS:
                    onUnloadSoundEffects();//不加载音效
                    break;
            	 case MSG_LOAD_SOUND_EFFECTS://加载音效
                    boolean loaded = onLoadSoundEffects();//调用onLoadSoundEffects加载音效,并将加载结果赋值给loaded
                    if (msg.obj != null) {
                        LoadSoundEffectReply reply = (LoadSoundEffectReply)msg.obj;
                        synchronized (reply) {
                            reply.mStatus = loaded ? 0 : -1;
                            reply.notify();
                        }
                    }
                    break;
              	...代码省略...
            }
    }
}

这里我们只要分析一下AudioService的加载音效资源的loadSoundEffects方法,该方法会调用sendMsg,发送类型为MSG_UNLOAD_SOUND_EFFECTS的msg给mAudioHandler。然后会进一步触发AudioHandler的handleMessage方法,该消息最终会触发onLoadSoundEffects方法。

4、AudioHandler的onLoadSoundEffects方法如下所示。

java 复制代码
public class AudioService extends IAudioService.Stub
        implements AccessibilityManager.TouchExplorationStateChangeListener,
            AccessibilityManager.AccessibilityServicesStateChangeListener {
    //音效资源文件名称
    private static final List<String> SOUND_EFFECT_FILES = new ArrayList<String>();
    
    private class AudioHandler extends Handler {
		//加载音效
        private boolean onLoadSoundEffects() {
            int status;
            synchronized (mSoundEffectsLock) {
            	//如果系统未启动完毕直接返回
                if (!mSystemReady) {
                    Log.w(TAG, "onLoadSoundEffects() called before boot complete");
                    return false;
                }
                //如果mSoundPool不为空直接返回
                if (mSoundPool != null) {
                    return true;
                }
                //加载触摸音效
                loadTouchSoundAssets();
   				...代码暂时省略...
        }
        
    //加载按键音效资源
    private void loadTouchSoundAssets() {
        XmlResourceParser parser = null;
        //如果音效资源文件列表不为空直接返回
        if (!SOUND_EFFECT_FILES.isEmpty()) {
            return;
        }
        //加载按键默认的音效资源
        loadTouchSoundAssetDefaults();
       ...代码省略...
	}
	
    private void loadTouchSoundAssetDefaults() {
    	//在类型为List<String>的SOUND_EFFECT_FILES中添加默认的按键音效资源Effect_Tick.ogg
        SOUND_EFFECT_FILES.add("Effect_Tick.ogg");
        for (int i = 0; i < AudioManager.NUM_SOUND_EFFECTS; i++) {
            SOUND_EFFECT_FILES_MAP[i][0] = 0;
            SOUND_EFFECT_FILES_MAP[i][1] = -1;
        }
    }
}

onLoadSoundEffects方法首先判断系统是否已经启动完毕,如果未启动直接返回;然后判断mSoundPool是否空,如果不为空则直接返回;

然后首先会调用一个关键方法loadTouchSoundAssets,该方法首先判断音效资源文件列表SOUND_EFFECT_FILES是否为空,不为空直接返回。如果以上判断都不成立,则会调用loadTouchSoundAssetDefaults方法加载按键默认的音效资源,该方法首先在SOUND_EFFECT_FILES的添加音效资源Effect_Tick.ogg。

5、继续往下看AudioHandler的onLoadSoundEffects方法。

java 复制代码
public class AudioService extends IAudioService.Stub
        implements AccessibilityManager.TouchExplorationStateChangeListener,
            AccessibilityManager.AccessibilityServicesStateChangeListener {
    //音效资源文件名称
    private static final List<String> SOUND_EFFECT_FILES = new ArrayList<String>();
    
    private class AudioHandler extends Handler {
		//加载音效
        private boolean onLoadSoundEffects() {
            int status;

            synchronized (mSoundEffectsLock) {
                if (!mSystemReady) {
                    Log.w(TAG, "onLoadSoundEffects() called before boot complete");
                    return false;
                }

                if (mSoundPool != null) {
                    return true;
                }
                //加载触摸音效
                loadTouchSoundAssets();
                //创建SoundPool对象
                mSoundPool = new SoundPool.Builder()
                        .setMaxStreams(NUM_SOUNDPOOL_CHANNELS)
                        .setAudioAttributes(new AudioAttributes.Builder()
                            .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
                            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                            .build())
                        .build();
         		...代码省略...
                int numSamples = 0;
                for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) {
                    // Do not load sample if this effect uses the MediaPlayer
                    if (SOUND_EFFECT_FILES_MAP[effect][1] == 0) {
                        continue;
                    }
                    if (poolId[SOUND_EFFECT_FILES_MAP[effect][0]] == -1) {
                        //获取音效资源文件路径
                        String filePath = getSoundEffectFilePath(effect);
                        //使用SoundPool加载音效资源文件
                        int sampleId = mSoundPool.load(filePath, 0);
                        if (sampleId <= 0) {
                            Log.w(TAG, "Soundpool could not load file: "+filePath);
                        } else {
                            SOUND_EFFECT_FILES_MAP[effect][1] = sampleId;
                            poolId[SOUND_EFFECT_FILES_MAP[effect][0]] = sampleId;
                            numSamples++;
                        }
                    } else {
                        SOUND_EFFECT_FILES_MAP[effect][1] =
                                poolId[SOUND_EFFECT_FILES_MAP[effect][0]];
                    }
                }
			...代码省略...
        }
        
        //获取音效资源文件路径,默认返回的音效资源文件路径为/system/media/audio/ui/Effect_Tick.ogg
        private String getSoundEffectFilePath(int effectType) {
            //  /product + /media/audio/ui/ + Effect_Tick.ogg
            String filePath = Environment.getProductDirectory() + SOUND_EFFECTS_PATH
                    + SOUND_EFFECT_FILES.get(SOUND_EFFECT_FILES_MAP[effectType][0]);
            if (!new File(filePath).isFile()) {
                //   /system + /media/audio/ui/ + Effect_Tick.ogg
                filePath = Environment.getRootDirectory() + SOUND_EFFECTS_PATH
                        + SOUND_EFFECT_FILES.get(SOUND_EFFECT_FILES_MAP[effectType][0]);
            }
            return filePath;
        }
 	 }
 }

onLoadSoundEffects先是调用loadTouchSoundAssets方法加载默认的音效资源文件名称,然后构建SoundPool实例对象,随后调用getSoundEffectFilePath获取按键音效资源文件路径,默认返回的音效资源文件路径为/system/media/audio/ui/Effect_Tick.ogg,并调用SoundPool加载该音效资源。

6、简单回顾一下以上步骤。

二、点击控件,播放音效资源

在系统开启按键音效之后,当我们点击任意控件之后,都会触发按键音效。接下来我们将会结合View的系统源码来梳理该流程。

1、当我们点击一个控件的时候,首先会触发该View的performClick方法。

frameworks/base/core/java/android/view/View.java

java 复制代码
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
	
    public boolean performClick() {
        // We still need to call this method to handle the cases where performClick() was called
        // externally, instead of through performClickInternal()
        notifyAutofillManagerOnClick();

        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);//播放按键点击音效
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }
    
    public void playSoundEffect(int soundConstant) {
        //判断mAttachInfo.mRootCallbacks是否为空,以及系统是否开启了按键音效
        if (mAttachInfo == null || mAttachInfo.mRootCallbacks == null || !isSoundEffectsEnabled()) {
            return;
        }
        //调用mAttachInfo.mRootCallbacks的playSoundEffect方法
        mAttachInfo.mRootCallbacks.playSoundEffect(soundConstant);
    }
 }

View的performClick方法会调用playSoundEffect方法,playSoundEffect方法首先判断mAttachInfo.mRootCallbacks是否为空,以及系统是否开启了按键音效,然后调用mAttachInfo.mRootCallbacks的playSoundEffect方法。我们知道WindowManager在将View添加到窗口的过程中,都需要用到ViewRootImpl这个类,具体请参考Android 9.0系统源码_窗口管理(二)WindowManager对窗口的管理过程这篇文章。

2、mAttachInfo最初是在ViewRootImpl的构造方法中被创建的。

frameworks/base/core/java/android/view/ViewRootImpl.java

java 复制代码
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
        
    public ViewRootImpl(Context context, Display display) {
        mContext = context;
        mWindowSession = WindowManagerGlobal.getWindowSession();
        mDisplay = display;
        mBasePackageName = context.getBasePackageName();
        mThread = Thread.currentThread();
        mLocation = new WindowLeaked(null);
        mLocation.fillInStackTrace();
        mWidth = -1;
        mHeight = -1;
        mDirty = new Rect();
        mTempRect = new Rect();
        mVisRect = new Rect();
        mWinFrame = new Rect();
        mWindow = new W(this);
        mTargetSdkVersion = context.getApplicationInfo().targetSdkVersion;
        mViewVisibility = View.GONE;
        mTransparentRegion = new Region();
        mPreviousTransparentRegion = new Region();
        mFirst = true; // true for the first time the view is added
        mAdded = false;
        //创建AttachInfo对象,倒数第二个参数就是View的playSoundEffect方法所用到的回调对象
        mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this, context);
		...代码省略...
    }
}

3、看完ViewRootImpl的构造方法,再来看下AttachInfo的构造方法。

java 复制代码
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
        
    final static class AttachInfo {

		//关键回调接口
        interface Callbacks {
        	//播放音效
            void playSoundEffect(int effectId);
            boolean performHapticFeedback(int effectId, boolean always);
        }

        final Callbacks mRootCallbacks;

        AttachInfo(IWindowSession session, IWindow window, Display display,
                ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,
                Context context) {
            mSession = session;
            mWindow = window;
            mWindowToken = window.asBinder();
            mDisplay = display;
            mViewRootImpl = viewRootImpl;
            mHandler = handler;
            mRootCallbacks = effectPlayer;//View的playSoundEffect方法所用到的回调对象就是这个
            mTreeObserver = new ViewTreeObserver(context);
        }
    }
 }

AttachInfo构造方法的最后一个参数很关键,因为View的playSoundEffect方法所调用的对象就是这个,结合ViewRootImpl的代码我们可以知道是ViewRootImpl实现了这个回调。

4、ViewRootImpl的playSoundEffect方法如下所示。

java 复制代码
public final class ViewRootImpl implements ViewParent, View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
        
    public void playSoundEffect(int effectId) {
        checkThread();//检测是否是UI线程
        try {
            final AudioManager audioManager = getAudioManager();

            switch (effectId) {
                case SoundEffectConstants.CLICK://播放按键点击音效
                    audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK);
                    return;
                case SoundEffectConstants.NAVIGATION_DOWN:
                    audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_DOWN);
                    return;
                case SoundEffectConstants.NAVIGATION_LEFT:
                    audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_LEFT);
                    return;
                case SoundEffectConstants.NAVIGATION_RIGHT:
                    audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_RIGHT);
                    return;
                case SoundEffectConstants.NAVIGATION_UP:
                    audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_UP);
                    return;
                default:
                    throw new IllegalArgumentException("unknown effect id " + effectId +
                            " not defined in " + SoundEffectConstants.class.getCanonicalName());
            }
        } catch (IllegalStateException e) {
            // Exception thrown by getAudioManager() when mView is null
            Log.e(mTag, "FATAL EXCEPTION when attempting to play sound effect: " + e);
            e.printStackTrace();
        }
    }
}

ViewRootImpl的playSoundEffect方法首先会检测一下当前线程是不是UI线程,然后会根据传入的effectId类型来判断要播放那种音效。因为View的performClick方法传入的是SoundEffectConstants.CLICK,所以会触发audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK)。

4、AudioManager的playSoundEffect方法如下所示。

java 复制代码
public class AudioManager {
    public void  playSoundEffect(int effectType) {
        //检测音效类型是否合规
        if (effectType < 0 || effectType >= NUM_SOUND_EFFECTS) {
            return;
        }
        //确定音效是否可用
        if (!querySoundEffectsEnabled(Process.myUserHandle().getIdentifier())) {
            return;
        }
        //获取AudioService服务
        final IAudioService service = getService();
        try {
            //调用服务的playSoundEffect方法
            service.playSoundEffect(effectType);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }
    
    /**
     * Settings has an in memory cache, so this is fast.
     */
    private boolean querySoundEffectsEnabled(int user) {
        return Settings.System.getIntForUser(getContext().getContentResolver(),
                Settings.System.SOUND_EFFECTS_ENABLED, 0, user) != 0;
    }
}

AudioManager的playSoundEffect会做一些校验,如果校验通过则会获取AudioService服务对象,并调用该对象的playSoundEffect方法进行音效播放。

5、AudioService和playSoundEffect相关的代码如下所示。

java 复制代码
public class AudioService extends IAudioService.Stub
        implements AccessibilityManager.TouchExplorationStateChangeListener,
            AccessibilityManager.AccessibilityServicesStateChangeListener {
            
    /**
     * 播放音效
     * @param effectType
     */
    public void playSoundEffect(int effectType) {
        playSoundEffectVolume(effectType, -1.0f);
    }

    public void playSoundEffectVolume(int effectType, float volume) {
        // do not try to play the sound effect if the system stream is muted
        if (isStreamMutedByRingerOrZenMode(STREAM_SYSTEM)) {
            return;
        }

        if (effectType >= AudioManager.NUM_SOUND_EFFECTS || effectType < 0) {
            Log.w(TAG, "AudioService effectType value " + effectType + " out of range");
            return;
        }
		//发送播放音效的消息给mAudioHandler
        sendMsg(mAudioHandler, MSG_PLAY_SOUND_EFFECT, SENDMSG_QUEUE,
                effectType, (int) (volume * 1000), null, 0);
    }
    
    private class AudioHandler extends Handler {
    		//加载音效
            private boolean onLoadSoundEffects() {
            	...代码暂时省略...
            }
            @Override
        	public void handleMessage(Message msg) {
            switch (msg.what) {
            	...代码省略...
            	 case MSG_UNLOAD_SOUND_EFFECTS:
                    onUnloadSoundEffects();//不加载音效
                    break;
            	 case MSG_LOAD_SOUND_EFFECTS://加载音效
	               boolean loaded = onLoadSoundEffects();//调用onLoadSoundEffects加载音效,并将加载结果赋值给loaded
            		...代码省略...
                    break;
                case MSG_PLAY_SOUND_EFFECT://播放音效
                    onPlaySoundEffect(msg.arg1, msg.arg2);
                    break;
              	...代码省略...
            }
    }
}

AudioService的playSoundEffect方法进一步调用playSoundEffectVolume,该方法会发送播放音效的消息MSG_PLAY_SOUND_EFFECT给mAudioHandler,最终会触发onPlaySoundEffect方法。

6、AudioService的onPlaySoundEffec方法如下所示。

java 复制代码
public class AudioService extends IAudioService.Stub
        implements AccessibilityManager.TouchExplorationStateChangeListener,
            AccessibilityManager.AccessibilityServicesStateChangeListener {
            
        private void onPlaySoundEffect(int effectType, int volume) {
            synchronized (mSoundEffectsLock) {

                onLoadSoundEffects();//加载音效

                if (mSoundPool == null) {
                    return;
                }
                float volFloat;
                // use default if volume is not specified by caller
                if (volume < 0) {
                    volFloat = (float)Math.pow(10, (float)sSoundEffectVolumeDb/20);
                } else {
                    volFloat = volume / 1000.0f;
                }

                if (SOUND_EFFECT_FILES_MAP[effectType][1] > 0) {
                	//通过SoundPool播放音效
                    mSoundPool.play(SOUND_EFFECT_FILES_MAP[effectType][1],
                                        volFloat, volFloat, 0, 0, 1.0f);
                } else {
                    //通过MediaPlayer播放音效
                    MediaPlayer mediaPlayer = new MediaPlayer();
                    try {
                        String filePath = getSoundEffectFilePath(effectType);
                        mediaPlayer.setDataSource(filePath);
                        mediaPlayer.setAudioStreamType(AudioSystem.STREAM_SYSTEM);
                        mediaPlayer.prepare();
                        mediaPlayer.setVolume(volFloat);
                        mediaPlayer.setOnCompletionListener(new OnCompletionListener() {
                            public void onCompletion(MediaPlayer mp) {
                                cleanupPlayer(mp);
                            }
                        });
                        mediaPlayer.setOnErrorListener(new OnErrorListener() {
                            public boolean onError(MediaPlayer mp, int what, int extra) {
                                cleanupPlayer(mp);
                                return true;
                            }
                        });
                        mediaPlayer.start();
                    } catch (IOException ex) {
                        Log.w(TAG, "MediaPlayer IOException: "+ex);
                    } catch (IllegalArgumentException ex) {
                        Log.w(TAG, "MediaPlayer IllegalArgumentException: "+ex);
                    } catch (IllegalStateException ex) {
                        Log.w(TAG, "MediaPlayer IllegalStateException: "+ex);
                    }
                }
            }
        }
   }

7、简单回顾一下以上步骤。

相关推荐
闲暇部落1 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
诸神黄昏EX3 小时前
Android 分区相关介绍
android
大白要努力!4 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee4 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
Winston Wood4 小时前
Perfetto学习大全
android·性能优化·perfetto
简鹿办公6 小时前
如何提取某站 MV 视频中的音乐为 MP3 音频
音视频·简鹿视频格式转换器·视频提取mp3音频
yufengxinpian6 小时前
集成了高性能ARM Cortex-M0+处理器的一款SimpleLink 2.4 GHz无线模块-RF-BM-2340B1
单片机·嵌入式硬件·音视频·智能硬件
Dnelic-7 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记
runing_an_min7 小时前
ffmpeg视频滤镜:替换部分帧-freezeframes
ffmpeg·音视频·freezeframes
Eastsea.Chen9 小时前
MTK Android12 user版本MtkLogger
android·framework