【Android】蓝牙开发——经典蓝牙(附Demo源码)

目录

一、前言

二、经典蓝牙的介绍

三、经典蓝牙项目实战

四、Demo案例源码地址:

 

一、前言

去年毕业进入公司以来,工作内容主要和蓝牙打交道,几个月的学习和实践让我这个Android蓝牙小白逐渐成长起来。但是,很多时候知识温故才能知新,每一次实践都会带来新的理解和体会。于是决定从今天开始,将这几个月以来的成长在博客中一一分享出来,给有需要的朋友作些参考,也欢迎大家提出指点和建议。

二、经典蓝牙的介绍

关于经典蓝牙的介绍,google官网上有详细的解释,此处贴上链接:https://developer.android.google.cn/guide/topics/connectivity/bluetooth

经典蓝牙的使用过程大致可分为以下几个步骤:

1、开启扫描,搜索周围蓝牙设备

2、扫描到设备后,与设备配对、建立连接

3、与设备成功连接后,实现数据通讯即收发数据

4、与设备通讯结束后,关闭与蓝牙的连接

三、经典蓝牙项目实战

1、在开始使用蓝牙之前,我们必须要声明两个权限,第一个是蓝牙权限,第二个是位置权限。

(1)蓝牙权限

    <!-- 应用使用蓝牙的权限 -->
    <uses-permission android:name="android.permission.BLUETOOTH"/>
    <!--启动设备发现或操作蓝牙设置的权限-->
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>

(2)位置权限(注意:Android 6.0以上版本还需要动态申请位置权限!)

<!--位置权限-->
<!--Android 10以上系统,需要ACCESS_FINE_LOCATION-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!--Android 9以及以下系统,需要ACCESS_FINE_LOCATION-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

2、创建发起蓝牙连接的线程ConnectThread.java 和 管理蓝牙连接、收发数据的线程ConnectedThread.java。

(1)ConnectThread.java,代码中已经有详细的注释。

package yc.bluetooth.androidbt;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.util.Log;

import java.io.IOException;
import java.util.UUID;


/**
 * 发起蓝牙连接
 */
public class ConnectThread extends Thread {
    private static final String TAG = "ConnectThread";
    private final BluetoothAdapter mBluetoothAdapter;
    private BluetoothSocket mmSocket;
    private final BluetoothDevice mmDevice;

    public ConnectThread(BluetoothAdapter bluetoothAdapter,BluetoothDevice bluetoothDevice,String uuid)  {
       this.mBluetoothAdapter = bluetoothAdapter;
       this.mmDevice = bluetoothDevice;

       //使用一个临时变量,等会赋值给mmSocket
       //因为mmSocket是静态的
       BluetoothSocket tmp = null ;

       if(mmSocket != null){
           Log.e(TAG,"ConnectThread-->mmSocket != null先去释放");
           try {
               mmSocket.close();
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
        Log.d(TAG,"ConnectThread-->mmSocket != null已释放");

        //1、获取BluetoothSocket
        try {
            //建立安全的蓝牙连接,会弹出配对框
            tmp = mmDevice.createRfcommSocketToServiceRecord(UUID.fromString(uuid));

        } catch (IOException e) {
           Log.e(TAG,"ConnectThread-->获取BluetoothSocket异常!" + e.getMessage());
        }

        mmSocket = tmp;
        if(mmSocket != null){
            Log.w(TAG,"ConnectThread-->已获取BluetoothSocket");
        }

    }

    @Override
    public void run(){

        //连接之前先取消发现设备,否则会大幅降低连接尝试的速度,并增加连接失败的可能性
        if(mBluetoothAdapter == null){
            Log.e(TAG,"ConnectThread:run-->mBluetoothAdapter == null");
            return;
        }
        //取消发现设备
        if(mBluetoothAdapter.isDiscovering()){
            mBluetoothAdapter.cancelDiscovery();
        }

        if(mmSocket == null){
            Log.e(TAG,"ConnectThread:run-->mmSocket == null");
            return;
        }

        //2、通过socket去连接设备
        try {
            Log.d(TAG,"ConnectThread:run-->去连接...");
            if(onBluetoothConnectListener != null){
                onBluetoothConnectListener.onStartConn();  //开始去连接回调
            }
            mmSocket.connect();  //connect()为阻塞调用,连接失败或 connect() 方法超时(大约 12 秒之后),它将会引发异常

            if(onBluetoothConnectListener != null){
                onBluetoothConnectListener.onConnSuccess(mmSocket);  //连接成功回调
                Log.w(TAG,"ConnectThread:run-->连接成功");
            }

        } catch (IOException e) {
            Log.e(TAG,"ConnectThread:run-->连接异常!" + e.getMessage());

            if(onBluetoothConnectListener != null){
                onBluetoothConnectListener.onConnFailure("连接异常:" + e.getMessage());
            }

            //释放
            cancel();
        }

    }

    /**
     * 释放
     */
    public void cancel() {
        try {
            if (mmSocket != null && mmSocket.isConnected()) {
                Log.d(TAG,"ConnectThread:cancel-->mmSocket.isConnected() = " + mmSocket.isConnected());
                mmSocket.close();
                mmSocket = null;
                return;
            }

            if (mmSocket != null) {
                mmSocket.close();
                mmSocket = null;
            }

            Log.d(TAG,"ConnectThread:cancel-->关闭已连接的套接字释放资源");

        } catch (IOException e) {
            Log.e(TAG,"ConnectThread:cancel-->关闭已连接的套接字释放资源异常!" + e.getMessage());
        }
    }

    private OnBluetoothConnectListener onBluetoothConnectListener;

    public void setOnBluetoothConnectListener(OnBluetoothConnectListener onBluetoothConnectListener) {
        this.onBluetoothConnectListener = onBluetoothConnectListener;
    }

    //连接状态监听者
    public interface OnBluetoothConnectListener{
        void onStartConn();  //开始连接
        void onConnSuccess(BluetoothSocket bluetoothSocket);  //连接成功
        void onConnFailure(String errorMsg);  //连接失败
    }

}

(2)ConnectedThread.java,代码中已经有详细的注释。

package yc.bluetooth.androidbt;

import android.bluetooth.BluetoothSocket;
import android.util.Log;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;


/**
 * 管理连接
 * 1、发送数据
 * 2、接收数据
 */
public class ConnectedThread extends Thread{
    private static final String TAG = "ConnectedThread";
    private BluetoothSocket mmSocket;
    private InputStream mmInStream;
    private OutputStream mmOutStream;
    //是否是主动断开
    private boolean isStop = false;
    //发起蓝牙连接的线程
    private ConnectThread connectThread;

    public void terminalClose(ConnectThread connectThread){
        isStop = true;
        this.connectThread = connectThread;
    }

    public ConnectedThread(BluetoothSocket socket){
        mmSocket = socket;

        InputStream tmpIn = null;
        OutputStream tmpOut = null;

        //使用临时对象获取输入和输出流,因为成员流是静态类型

        //1、获取 InputStream 和 OutputStream
        try {
            tmpIn = socket.getInputStream();
            tmpOut = socket.getOutputStream();

        } catch (IOException e) {
            Log.e(TAG,"ConnectedThread-->获取InputStream 和 OutputStream异常!");
        }

        mmInStream = tmpIn;
        mmOutStream = tmpOut;

        if(mmInStream != null){
            Log.d(TAG,"ConnectedThread-->已获取InputStream");
        }

        if(mmOutStream != null){
            Log.d(TAG,"ConnectedThread-->已获取OutputStream");
        }

    }

    public void run(){
        //最大缓存区 存放流
        byte[] buffer = new byte[1024 * 2];  //buffer store for the stream
        //从流的read()方法中读取的字节数
        int bytes = 0;  //bytes returned from read()

        //持续监听输入流直到发生异常
        while(!isStop){
            try {

                if(mmInStream == null){
                    Log.e(TAG,"ConnectedThread:run-->输入流mmInStream == null");
                    break;
                }
                //先判断是否有数据,有数据再读取
                if(mmInStream.available() != 0){
                    //2、接收数据
                    bytes = mmInStream.read(buffer);  //从(mmInStream)输入流中(读取内容)读取的一定数量字节数,并将它们存储到缓冲区buffer数组中,bytes为实际读取的字节数
                    byte[] b = Arrays.copyOf(buffer,bytes);  //存放实际读取的数据内容
                    Log.w(TAG,"ConnectedThread:run-->收到消息,长度" + b.length + "->" + bytes2HexString(b, b.length));  //有空格的16进制字符串
                    if(onSendReceiveDataListener != null){
                        onSendReceiveDataListener.onReceiveDataSuccess(b);  //成功收到消息
                    }
                }

            } catch (IOException e) {
                Log.e(TAG,"ConnectedThread:run-->接收消息异常!" + e.getMessage());
                if(onSendReceiveDataListener != null){
                    onSendReceiveDataListener.onReceiveDataError("接收消息异常:" + e.getMessage());  //接收消息异常
                }
                //关闭流和socket
                boolean isClose = cancel();
                if(isClose){
                    Log.e(TAG,"ConnectedThread:run-->接收消息异常,成功断开连接!");
                }
                break;
            }
        }
        //关闭流和socket
        boolean isClose = cancel();
        if(isClose){
            Log.d(TAG,"ConnectedThread:run-->接收消息结束,断开连接!");
        }
    }

    //发送数据
    public boolean write(byte[] bytes){
        try {

            if(mmOutStream == null){
                Log.e(TAG, "mmOutStream == null");
                return false;
            }

            //发送数据
            mmOutStream.write(bytes);
            Log.d(TAG, "写入成功:"+ bytes2HexString(bytes, bytes.length));
            if(onSendReceiveDataListener != null){
                onSendReceiveDataListener.onSendDataSuccess(bytes);  //发送数据成功回调
            }
            return true;

        } catch (IOException e) {
            Log.e(TAG, "写入失败:"+ bytes2HexString(bytes, bytes.length));
            if(onSendReceiveDataListener != null){
                onSendReceiveDataListener.onSendDataError(bytes,"写入失败");  //发送数据失败回调
            }
            return false;
        }
    }

    /**
     * 释放
     * @return   true 断开成功  false 断开失败
     */
    public boolean cancel(){
        try {
            if(mmInStream != null){
                mmInStream.close();  //关闭输入流
            }
            if(mmOutStream != null){
                mmOutStream.close();  //关闭输出流
            }
            if(mmSocket != null){
                mmSocket.close();   //关闭socket
            }
            if(connectThread != null){
                connectThread.cancel();
            }

            connectThread = null;
            mmInStream = null;
            mmOutStream = null;
            mmSocket = null;

            Log.w(TAG,"ConnectedThread:cancel-->成功断开连接");
            return true;

        } catch (IOException e) {
            // 任何一部分报错,都将强制关闭socket连接
            mmInStream = null;
            mmOutStream = null;
            mmSocket = null;

            Log.e(TAG, "ConnectedThread:cancel-->断开连接异常!" + e.getMessage());
            return false;
        }
    }

    /**
     * 字节数组-->16进制字符串
     * @param b   字节数组
     * @param length  字节数组长度
     * @return 16进制字符串 有空格类似“0A D5 CD 8F BD E5 F8”
     */
    public static String bytes2HexString(byte[] b, int length) {
        StringBuffer result = new StringBuffer();
        String hex;
        for (int i = 0; i < length; i++) {
            hex = Integer.toHexString(b[i] & 0xFF);
            if (hex.length() == 1) {
                hex = '0' + hex;
            }
            result.append(hex.toUpperCase()).append(" ");
        }
        return result.toString();
    }

    private OnSendReceiveDataListener onSendReceiveDataListener;

    public void setOnSendReceiveDataListener(OnSendReceiveDataListener onSendReceiveDataListener) {
        this.onSendReceiveDataListener = onSendReceiveDataListener;
    }

    //收发数据监听者
    public interface OnSendReceiveDataListener{
        void onSendDataSuccess(byte[] data);  //发送数据结束
        void onSendDataError(byte[] data, String errorMsg); //发送数据出错
        void onReceiveDataSuccess(byte[] buffer);  //接收到数据
        void onReceiveDataError(String errorMsg);   //接收数据出错
    }



}

3、使用蓝牙之前,首先要检查当前手机是否支持蓝牙。如果支持蓝牙,检查手机蓝牙是否已开启。如果没有开启,则需要先打开蓝牙。打开手机蓝牙,有两种方式,推荐使用第二种打开方式。

private void initBluetooth() {
        bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        if(bluetoothAdapter == null){
            Toast.makeText(this, "当前手机设备不支持蓝牙", Toast.LENGTH_SHORT).show();
        }else{
            //手机设备支持蓝牙,判断蓝牙是否已开启
            if(bluetoothAdapter.isEnabled()){
                Toast.makeText(this, "手机蓝牙已开启", Toast.LENGTH_SHORT).show();
            }else{
                //蓝牙没有打开,去打开蓝牙。推荐使用第二种打开蓝牙方式
                //第一种方式:直接打开手机蓝牙,没有任何提示
//                bluetoothAdapter.enable();  //BLUETOOTH_ADMIN权限
                //第二种方式:友好提示用户打开蓝牙
                Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
                startActivity(enableBtIntent);
            }
        }
    }

4、确保手机蓝牙已打开,就可以开始搜索设备。搜索设备只需调用startDiscovery()方法,但搜索的结果是通过广播来获取的,所以,还需要定义广播来获取搜索到的设备。

(1)搜索设备

private void searchBtDevice() {
        if(bluetoothAdapter.isDiscovering()){ //当前正在搜索设备...
            return;
        }
        //开始搜索
        bluetoothAdapter.startDiscovery();
    }

(2)自定义广播接收器,接收搜索到的设备。

/**
     * 蓝牙广播接收器
     */
    private static class BtBroadcastReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (TextUtils.equals(action, BluetoothAdapter.ACTION_DISCOVERY_STARTED)) { //开启搜索
                if (onDeviceSearchListener != null) {
                    onDeviceSearchListener.onDiscoveryStart();  //开启搜索回调
                }

            } else if (TextUtils.equals(action, BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) {//完成搜素
                if (onDeviceSearchListener != null) {
                    onDeviceSearchListener.onDiscoveryStop();  //完成搜素回调
                }

            } else if (TextUtils.equals(action, BluetoothDevice.ACTION_FOUND)) {  //3.0搜索到设备
                //蓝牙设备
                BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                //信号强度
                int rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE);

                Log.d(TAG, "扫描到设备:" + bluetoothDevice.getName() + "-->" + bluetoothDevice.getAddress());
                if (onDeviceSearchListener != null) {
                    onDeviceSearchListener.onDeviceFound(bluetoothDevice,rssi);  //3.0搜素到设备回调
                }
            }
        }

        /**
         * 蓝牙设备搜索监听者
         * 1、开启搜索
         * 2、完成搜索
         * 3、搜索到设备
         */
        public interface OnDeviceSearchListener {
            void onDiscoveryStart();   //开启搜索
            void onDiscoveryStop();    //完成搜索
            void onDeviceFound(BluetoothDevice bluetoothDevice, int rssi);  //搜索到设备
        }

        private OnDeviceSearchListener onDeviceSearchListener;

        public void setOnDeviceSearchListener(OnDeviceSearchListener onDeviceSearchListener) {
            this.onDeviceSearchListener = onDeviceSearchListener;
        }
    }

(3)注册广播接收器

private void initBtBroadcast() {
        //注册广播接收
        btBroadcastReceiver = new BtBroadcastReceiver();
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED); //开始扫描
        intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);//扫描结束
        intentFilter.addAction(BluetoothDevice.ACTION_FOUND);//搜索到设备
        registerReceiver(btBroadcastReceiver,intentFilter);

    }

(4)注册过广播之后,要记得在onDestroy()中注销广播

@Override
    protected void onDestroy() {
        super.onDestroy();

        //注销广播接收
        unregisterReceiver(btBroadcastReceiver);
    }

5、搜索到目标设备之后,就可以与蓝牙设备建立连接。

(1)发起连接

 /**
     * 开始连接设备
     * @param bluetoothDevice   蓝牙设备
     * @param uuid               发起连接的UUID
     * @param conOutTime        连接超时时间
     */
    public void startConnectDevice(final BluetoothDevice bluetoothDevice, String uuid, long conOutTime){
        if(bluetoothDevice == null){
            Log.e(TAG,"startConnectDevice-->bluetoothDevice == null");
            return;
        }
        if(bluetoothAdapter == null){
            Log.e(TAG,"startConnectDevice-->bluetooth3Adapter == null");
            return;
        }
        //发起连接
        connectThread = new ConnectThread(bluetoothAdapter,curBluetoothDevice,uuid);
        connectThread.setOnBluetoothConnectListener(new ConnectThread.OnBluetoothConnectListener() {
            @Override
            public void onStartConn() {
                Log.d(TAG,"startConnectDevice-->开始连接..." + bluetoothDevice.getName() + "-->" + bluetoothDevice.getAddress());
            }


            @Override
            public void onConnSuccess(BluetoothSocket bluetoothSocket) {
                //移除连接超时
                mHandler.removeCallbacks(connectOuttimeRunnable);
                Log.d(TAG,"startConnectDevice-->移除连接超时");
                Log.w(TAG,"startConnectDevice-->连接成功");

                Message message = new Message();
                message.what = CONNECT_SUCCESS;
                mHandler.sendMessage(message);

                //标记当前连接状态为true
                curConnState = true;
                //管理连接,收发数据
                managerConnectSendReceiveData(bluetoothSocket);
            }

            @Override
            public void onConnFailure(String errorMsg) {
                Log.e(TAG,"startConnectDevice-->" + errorMsg);

                Message message = new Message();
                message.what = CONNECT_FAILURE;
                mHandler.sendMessage(message);

                //标记当前连接状态为false
                curConnState = false;

                //断开管理连接
                clearConnectedThread();
            }
        });

        connectThread.start();
        //设置连接超时时间
        mHandler.postDelayed(connectOuttimeRunnable,conOutTime);

    }

    //连接超时
    private Runnable connectOuttimeRunnable = new Runnable() {
        @Override
        public void run() {
            Log.e(TAG,"startConnectDevice-->连接超时" );

            Message message = new Message();
            message.what = CONNECT_FAILURE;
            mHandler.sendMessage(message);

            //标记当前连接状态为false
            curConnState = false;
            //断开管理连接
            clearConnectedThread();
        }
    };

(2)管理连接,数据收发

 managerConnectSendReceiveData()方法中,connectedThread对象进行数据发送结果、接收结果监听。

/**
     * 管理已建立的连接,收发数据
     * @param bluetoothSocket   已建立的连接
     */
    public void managerConnectSendReceiveData(BluetoothSocket bluetoothSocket){
        //管理已有连接
        connectedThread = new ConnectedThread(bluetoothSocket);
        connectedThread.start();
        connectedThread.setOnSendReceiveDataListener(new ConnectedThread.OnSendReceiveDataListener() {
            @Override
            public void onSendDataSuccess(byte[] data) {
                Log.w(TAG,"发送数据成功,长度" + data.length + "->" + bytes2HexString(data,data.length));
                Message message = new Message();
                message.what = SEND_SUCCESS;
                message.obj = "发送数据成功,长度" + data.length + "->" + bytes2HexString(data,data.length);
                mHandler.sendMessage(message);
            }

            @Override
            public void onSendDataError(byte[] data,String errorMsg) {
                Log.e(TAG,"发送数据出错,长度" + data.length + "->" + bytes2HexString(data,data.length));
                Message message = new Message();
                message.what = SEND_FAILURE;
                message.obj = "发送数据出错,长度" + data.length + "->" + bytes2HexString(data,data.length);
                mHandler.sendMessage(message);
            }

            @Override
            public void onReceiveDataSuccess(byte[] buffer) {
                Log.w(TAG,"成功接收数据,长度" + buffer.length + "->" + bytes2HexString(buffer,buffer.length));
                Message message = new Message();
                message.what = RECEIVE_SUCCESS;
                message.obj = "成功接收数据,长度" + buffer.length + "->" + bytes2HexString(buffer,buffer.length);
                mHandler.sendMessage(message);
            }

            @Override
            public void onReceiveDataError(String errorMsg) {
                Log.e(TAG,"接收数据出错:" + errorMsg);
                Message message = new Message();
                message.what = RECEIVE_FAILURE;
                message.obj = "接收数据出错:" + errorMsg;
                mHandler.sendMessage(message);
            }
        });
    }

 sendData()方法中,connectedThread对象发送数据。

 /**
     * 发送数据
     * @param data      要发送的数据 字符串
     * @param isHex     是否是16进制字符串
     * @return   true 发送成功  false 发送失败
     */
    public boolean sendData(String data,boolean isHex){
        if(connectedThread == null){
            Log.e(TAG,"sendData:string -->connectedThread == null");
            return false;
        }
        if(data == null || data.length() == 0){
            Log.e(TAG,"sendData:string-->要发送的数据为空");
            return false;
        }

        if(isHex){  //是16进制字符串
            data.replace(" ","");  //取消空格
            //检查16进制数据是否合法
            if(data.length() % 2 != 0){
                //不合法,最后一位自动填充0
                String lasts = "0" + data.charAt(data.length() - 1);
                data = data.substring(0,data.length() - 2) + lasts;
            }
            Log.d(TAG,"sendData:string -->准备写入:" + data);  //加空格显示
            return connectedThread.write(hexString2Bytes(data));
        }

        //普通字符串
        Log.d(TAG,"sendData:string -->准备写入:" + data);
        return connectedThread.write(data.getBytes());
    }

6、与蓝牙设备通讯结束之后,可与蓝牙设备断开连接。

/**
     * 断开已有的连接
     */
    public void clearConnectedThread(){
        Log.d(TAG,"clearConnectedThread-->即将断开");

        //connectedThread断开已有连接
        if(connectedThread == null){
            Log.e(TAG,"clearConnectedThread-->connectedThread == null");
            return;
        }
        connectedThread.terminalClose(connectThread);

        //等待线程运行完后再断开
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                connectedThread.cancel();  //释放连接

                connectedThread = null;
            }
        },10);

        Log.w(TAG,"clearConnectedThread-->成功断开连接");
        Message message = new Message();
        message.what = DISCONNECT_SUCCESS;
        mHandler.sendMessage(message);

    }

7、项目演示

(1)扫描到设备,点击“连接”按钮, 会在“搜索”按钮下方显示连接结果 。注意经典蓝牙连接是,第一次连接时会有弹出一个配对框,这个具体配对方式是蓝牙设备开发人员设置的。

(2)手机给蓝牙设备(设备名为:BTyqy)发送数据成功之后,蓝牙设备把接收到的数据再回发送给手机。

(3)断开连接。点击“断开”按钮, 会在“搜索”按钮下方显示断开结果 。

四、Demo案例源码地址:

注意:源码中没有进行位置权限的静态声明以及动态申请,小伙伴们使用时需要自己添加,谢谢!

CSDN:https://download.csdn.net/download/qq_38950819/11615060

码云:https://gitee.com/lilium_foliage/Android-Bluetooth

评论 62
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值