Android USB 开发

基本概念和术语

USB是一个用于数据交换的总线(Bus),发起(initiate)数据交换的一方称为主机(host),另一方称为外设(peripheral),两者通过总线实现通信,由主机负责供电描述符(descriptor)里.一个外设就是一个单独的物理实体,但它却可能有多个逻辑上的设备功能(device function),比如一个网络摄像头,除了有照相机,还可能有内置的麦克风,这种外接设备也被称为复合设备(composite device)

主机通过管道(pipe)连接到外设的端点(endpoint),端点是指外设某一项功能的逻辑实体(logic entity),一条管道对应一个端点.一个外设最多可以有32个端点(16个进,16个出),主机会对端点初始化,给其分配相应的序号和功能,一经分配,不论管道是否关闭都不会改变.除了用于进行设备配置的0号端点,其他端点都被划分到不同的接口(interface)**,每一个接口对应一项外设功能.

管道有两种类型:

消息(message):用来进行双向(bi-directional)控制与状态获取,对传输的数据有结构上的要求.

流(stream):用来进行单向(uni-directional)数据传输,不会对传输的数据进行结构上的改动.

因为通信是由主机发启的,所以input和output都是相对主机而言的,输入就是外设传数据给主机,输出就是主机传数据给外设.

主机有一个关键任务称为枚举(enumeration),通过枚举主机获取所有外设的描述符(descriptor),给每一个外设分配一个地址(address)**并完成其他配置,比如前文所说对端点的初始化.经过枚举之后,外设就可以使用了,如果主机重启,就会对所有连接的外设重置并再次进行枚举.

USB的数据传输模式主要有四种:

control:提供无损传输,用来传输配置/状态/命令等通信,所有的USB设备都必须支持这种传输模式.

interrupt有限延迟下保证设备的快速反应,主要用于键盘等输入设备.

bulk:用于大量数据的无损传输,但不保证带宽和延迟,比如用U盘拷贝文件.

isochronous:保证尽可能快的传输速度,但可能会有数据丢失,比如实时的音视频传输.

常用类

Android 3.1(API级别12)以上原生提供了 USB 开发的 API,在android.hardware.usb包下提供了开发的相关类。

UsbManager 获得 USB 管理器,与连接的 USB 设备通信。

# 获取 UsbManager
usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
  • UsbDevice USB 设备的抽象,每个UsbDevice 都代表一个 USB 设备。
    该类的方法主要有
    getDeviceClass() 返回此USB设备的类别,用一个整型来表示
    getDeviceId() 返回唯一标识此设备的ID号,也用一个整型来表示
    getDeviceName() 返回此设备的名称,用一个字符串来表示
    getDeviceProtocol() 返回此设备的协议类别,用一个整型来表示
    getDeviceSubclass () 返回此设备的子类别,用一个整型来表示
    getVendorId() 返回生产商ID
    getProductId() 返回产品ID
    getInterfaceCount() 返回此设备的接口数量
    getInterface(int index) 得到此设备的一个接口,返回一个
# 获取 USB 设备列表
    public List<UsbDevice> getDeviceList() {
        HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList();
        Iterator<UsbDevice> deviceIterator = deviceList.values().iterator();
        List<UsbDevice> usbDevices = new ArrayList<>();
        while (deviceIterator.hasNext()) {
            UsbDevice device = deviceIterator.next();
            usbDevices.add(device);
        }
        return usbDevices;
    }
   #获取特定的设备
    /**
     * mVendorId=1137,mProductId=85  佳博 3150T 标签打印机
     *
     * @param vendorId  厂商ID
     * @param productId 产品ID
     * @return  device
     */
    public UsbDevice getUsbDevice(int vendorId, int productId) {
        HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList();
        Iterator<UsbDevice> deviceIterator = deviceList.values().iterator();
        while (deviceIterator.hasNext()) {
            UsbDevice device = deviceIterator.next();
            if (device.getVendorId() == vendorId && device.getProductId() == productId) {
                Log.e("USBUtil", "getDeviceList: " + device.getDeviceName());
                return device;
            }
        }
        Toast.makeText(context, "没有对应的设备", Toast.LENGTH_SHORT).show();
        return null;
    }

UsbInterface 表示一个为该设备定义了一个函数集的USB设备接口。 一个 UsbDevice 可能包含一个或多个UsbInterface,每个 Interface 都是独立的,一个接口,有两个端点,分别接受和发送数据
本身是一个类,并不是一个接口。此类的主要方法有:
getId() 得到给接口的ID 号
getInterfaceClass() 得到该接口的类别
getInterfaceSubclass () 得到该接口的子类
getInterfaceProtocol() 得到该接口的协议类别
getEndpointCount() 得到关于此接口的节点数量
getEndpoint(int index) 对于指定的index获得此接口的一个节点,返回一个

#一个usbDevice有多个UsbInterface,我们需要的一般是第一个
usbInterface=usbDevice.getInterface(0);

UsbEndpoint UsbEndpoint 是 interface 的通信通道。 代表一个接口的某个节点的类,该类主要提供了以下方法:
getAddress() 获得此节点的地址
getAttributes ( ) 获得此节点的属性
getDirection() 获得此节点的数据传输方向

UsbDeviceConnection 代表USB 连接的一个类。用此连接可以向USB 设备发送和接收数据,可以通过调用该方法openDevice(UsbDevice usbDevice)来得到该类的一个实例。该类提供了以下方法使用:

    bulkTransfer(UsbEndpoint endpoint , byte[] buffer , int length , int timeout)

通过给定的endpoint 来进行大量的数据传输,传输的方向取决于该节点的方向,buffer是要发送或接收的字节数组,length是该字节数组的长度。失败则返回负数

  controlTransfer( int requestType, int request , int value , int index , byte[] buffer , int length , int timeout)

该方法通过0节点向此设备传输数据,传输的方向取决于请求的类别,如果requestType 为 USB_DIR_OUT 则为写数据 , USB _DIR_IN ,则为读数据
UsbRequest USB 请求包。
UsbConstants USB 常量的定义

两种开发模式

Android设备最早都是以PC的USB外设形式而存在,主要功能就是Android fastboot 或Android Debug Bridge (adb),都是基于bulk传输模式的.

USB Host Mode

顾名思义,Android设备作为主机,需要安卓设备支持OTG接头.常见应用场景诸如连接数码相机,键盘,鼠标,游戏手柄等硬件.需要注意的是,通常安卓设备的电量作为主机是捉襟见肘的.

USB Accessory Mode

这种模式下Android设备承担外设的角色.应用场景诸如连接机器人控制器,音响,医疗器材等,当然前提是这些设备支持与Android设备连接并且遵守Android accessory communication protocol.这种模式可以让不具有host能力的Android设备与其他硬件交互.

注意:在谷歌的话语体系里,Host模式里与Android设备相连的硬件被称为USB device,Accessory模式里,与Android设备相连的硬件称为USB accessory.尽管是accessory是"附件"的意思,其承担的却是逻辑上主机(host)的角色.

Android通过两种模式支持各种 USB 外设和 Android USB 附件(实现Android附件协议的硬件):USB附件和USB主机。USB开发需 Android 3.1(API级别12)以上。

配置AndroidManifest.xml文件

配置好清单文件后当用户连接与您的设备过滤器匹配的设备时,系统会向他们显示一个对话框,询问他们是否要启动您的应用程序。如果用户接受,则应用程序将自动具有访问设备的权限,直到设备断开连接。如果给了默认,那么这个 USB 设备插入后会自动启动这个 Activity

在android系统中,默认是关闭usb模式的,当我们将usb外部设备插入android设备时,android系统不回出任何响应。如果我们要打开系统usb模式,需要在android清单文件里使用标签声明响应的usb模式

    #Host模式
    <uses-feature android:name="android.hardware.usb.host" />
    #accessory模式
    <uses-permission android:name="android.hardware.usb.accessory" />

配置usb设备插入的通知和相关usb设备的过滤

若我们希望在插入usb设备时,android系统能接收到提示性通知,则需要在android组件中配置和标签配置相关信息,中配置我们要接收的相关类型的信息通知,配置我们需要检测的指定的设备,标签内容对应一个xml资源文件,里面配置我们指定的usb设备。如下:

<activity  >
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    <!--host模式下通知的过滤-->
    <intent-filter>
        <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
    </intent-filter>
    <!--host模式下指定过滤设备的配置文件-->
    <meta-data
        android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
        android:resource="@xml/device_filter" />
    <!--accessory模式下通知的过滤-->
    <intent-filter>
        <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" />
    </intent-filter>
    <!--accessory模式下指定过滤设备的配置文件-->
    <meta-data
        android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"
        android:resource="@xml/device_filter" />

</activity>

配置 xml资源文件(用于过滤设备) :

在XML资源文件中,声明要过滤的USB设备的元素。以下列表描述了属性 。通常,如果要过滤特定设备并使用类,子类和协议(如果要过滤一组USB设备(如大容量存储设备或数码相机)),请使用供应商(vendor-id)和产品(product-id)ID,在开发中这些过滤ID一般可以在文档中找到,或者自己连上看也行。你可以指定部分或全部这些属性。
将资源文件保存在res/xml/目录中。资源文件名(不带.xml扩展名)必须与您在元素中指定的文件名相同 。对于XML资源文件格式的 例子如下:
<?xml version="1.0" encoding="utf-8"?>
<rescources xmlns:android="http://schemas.android.com/apk/res/android">
    <!--host模式-->
    <usb-device
        class=""
        vendor-id=""
        product-id=""
        protoclo=""
        subclass="" />

    <!--accessory模式-->
    <usb-accessory
        model="xxx"
        manufacturer="xxx" />

建立通信

无论是host模式还是accessory模式,在进行检测获取usb设备时,都需要用到UsbManager类,只是调用不同的方法而已。如下:
host模式

```
 #UsbManager
 UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
 #获取设备列表 
 HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList();
  #遍历设备,找到指定的productId和vendorId 设备
  Iterator<UsbDevice> deviceIterator = deviceList.values().iterator();
    while (deviceIterator.hasNext()) {
        UsbDevice device = deviceIterator.next();
        if (device.getVendorId() == vendorId && device.getProductId() == productId) {
      #获取当前UsbDevice
       mUsbDevice = device;
        # 检查mUsbDevice 否有对应权限,若无则请求权限,通过广播接收权限信息获取情况。
          if (!usbManager.hasPermission(mUsbDevice)) {//检测是否有usb权限
        requestPermission(mUsbDevice);
    } else {
      #与usb设备进行通信
        connect(mUsbDevice);
    }
        }

/**
 * 请求usb权限
 * @param usbManager
 * @param parcelable
 */
 private static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION";
 
public void requestPermission(UsbDevice usbDevice,UsbManager usbManager){       
            IntentFilter intentFilter = new IntentFilter(ACTION_USB_PERMISSION );
            registerReceiver(mMyBroadcastReceiver, intentFilter);
            PendingIntent broadcast = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION ), 0);
            usbManager.requestPermission(usbDevice, broadcast);

}

/**
* 权限广播接收器
*/
private BroadcastReceiver mMyBroadcastReceiver=new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (ACTION_USB_PERMISSION .equals(action)) {
synchronized (this) {
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
if (usbDevice != null) {
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
connect(usbDevice);
Toast.makeText(context, “success get permission”, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, “failure get permission”, Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(context, “the usbDevice be null”, Toast.LENGTH_SHORT).show();
}
}
}
}
};
public boolean connect(UsbDevice mUsbDevice) {
if (mUsbDevice != null) {
UsbInterface anInterface = mUsbDevice.getInterface(0);
mUsbDeviceConnection = mUsbManager.openDevice(mUsbDevice);//打开设备,获取 UsbDeviceConnection 对象,连接设备,用于后面的通讯
if (mUsbDeviceConnection == null) {
Toast.makeText(mContext, “mUsbDeviceConnection can’t be null”, Toast.LENGTH_SHORT).show();
return false;
}
if (mUsbDeviceConnection.claimInterface(anInterface, true)) {
Toast.makeText(mContext, “找到USB接口”, Toast.LENGTH_SHORT).show();
int endpointCount = anInterface.getEndpointCount();
for (int i = 0; i < endpointCount; i++) {
UsbEndpoint endpoint = anInterface.getEndpoint(i);
if (endpoint.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK) {
if (UsbConstants.USB_DIR_IN == endpoint.getDirection()) {
mUsbEndpoint_in = endpoint;//获取读数据通道
} else {
mUsbEndpoint_out = endpoint;//获取写数据通道
}
}
}
return true;
} else {
mUsbDeviceConnection.close();//关闭连接
Toast.makeText(mContext, “找不到USB接口”, Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(mContext, “mUsbDevice can’t be null”, Toast.LENGTH_SHORT).show();
}
return false;
}

/**
 * 从usb通信设备中读取数据
 * @return
 */
public byte[] readData() {
    int inMax = mUsbEndpoint_in.getMaxPacketSize();
    byte[] bytes = new byte[inMax];
    ByteBuffer byteBuffer = ByteBuffer.allocate(inMax);
    UsbRequest usbRequest = new UsbRequest();
    usbRequest.initialize(mUsbDeviceConnection, mUsbEndpoint_in);
    usbRequest.queue(byteBuffer, inMax);
    if (mUsbDeviceConnection.requestWait() == usbRequest) {
        bytes = byteBuffer.array();
    }
    return bytes;
}

/**
 * 将数据写入到usb设备中
 * @param bytes
 */
public void sendData(byte[] bytes) {
    if (mUsbDeviceConnection == null) {
        Toast.makeText(mContext, "mUsbDeviceConnection can't be null", Toast.LENGTH_SHORT).show();
        return;
    }
    if (mUsbEndpoint_out == null) {
        Toast.makeText(mContext, "mUsbEndpoint_out can't be null", Toast.LENGTH_SHORT).show();
        return;
    }

    int i = mUsbDeviceConnection.bulkTransfer(mUsbEndpoint_out, bytes, bytes.length, 1000);
    if (i < 0) {
        Toast.makeText(mContext, "failure to write", Toast.LENGTH_SHORT).show();
    } else {
        Toast.makeText(mContext, "success to write", Toast.LENGTH_SHORT).show();
    }
}



/**
 * 筛选出我们想要的usb设备
 * @param name
 * @param usbDevice
 * @return
 */
public boolean filterDevice(String name, UsbDevice usbDevice) {
    // TODO: 2019/3/13 对应判断设备是否是我们要连接的设备
    return false;
}
accessory模式(和host模式差不多,大致步骤一样,只是相关类不同而已如下)
1)获取UsbManager
2)通过usbManager获取连接到的usb外部设备UsbAccessory
3)获取usb设备引用,通过usbManager检测是否有对应权限,若无则请求权限,通过广播接收权限信息获取情况。
4)与UsbAccessory设备进行通信

public static final String action_usb_permission = "org.zhuhailong.myviewproject.permission";

private UsbManager usbManager;

public void hostAccessory(Context context) {

    //获取UsbManager
    usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);

    //获取usb设备列表
    UsbAccessory[] accessoryList = usbManager.getAccessoryList();

    if (accessoryList == null || accessoryList.length <= = 0) {
        Toast.makeText(context, "未查询到对应设备", Toast.LENGTH_SHORT).show();
        return;
    }

    UsbAccessory mUsbAccessory = null;
    for (UsbAccessory usbAccessory : accessoryList) {
        if (filterDevice(usbAccessory)) {//判断是否使我们要连接的usb设备
            break;
        }
    }

    if (usbManager.hasPermission(mUsbAccessory)) {//检测是否有usb权限
        requestPermission(mUsbAccessory);//请求usb权限
    } else {
        connect(mUsbAccessory);//连接usb设备
    }

}

/**
 * 请求usb权限
 *
 * @param usbManager
 * @param parcelable
 */
public void requestPermission(Parcelable parcelable, UsbManager usbManager) {
    UsbAccessory usbAccessory = (UsbAccessory) parcelable;
    if (!usbManager.hasPermission(usbAccessory)) {
        IntentFilter intentFilter = new IntentFilter(action_usb_permission);
        registerReceiver(mMyBroadcastReceiver, intentFilter);
        PendingIntent broadcast = PendingIntent.getBroadcast(this, 0, new Intent(action_usb_permission), 0);
        usbManager.requestPermission(usbAccessory, broadcast);
    } else {
        Toast.makeText(context, "already have permission", Toast.LENGTH_SHORT).show();
    }
}

private FileInputStream mFileInputStream;

private FileOutputStream mFileOutputStream;

/**
 * 连接设备
 * @param usbAccessory
 * @return
 */
public boolean connect(UsbAccessory usbAccessory) {
    ParcelFileDescriptor parcelFileDescriptor = usbManager.openAccessory(usbAccessory);
    if (parcelFileDescriptor != null) {
        FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();

        mFileInputStream = new FileInputStream(fileDescriptor);

        mFileOutputStream = new FileOutputStream(fileDescriptor);

    }
    return false;
}

/**
 * 写入数据
 * @param data
 */
public void write(byte[] data) {
    try {
        mFileOutputStream.write(data);
    } catch (IOException e) {
        e.printStackTrace();
    }

}

/**
 * 读取数据
 */
public void read() {
    int max;//字节数据根据协议来确定
    byte[] data = new byte[max];
    mFileInputStream.read(data);
}


/**
 * 权限广播接收器
 */
private BroadcastReceiver mMyBroadcastReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (action_usb_permission.equals(action)) {
            synchronized (this) {
                UsbAccessory usbAccessory = intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
                if (usbAccessory != null) {
                    if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                        connect(usbAccessory);
                        Toast.makeText(context, "success get permission", Toast.LENGTH_SHORT).show();
                    } else {
                        Toast.makeText(context, "failure get permission", Toast.LENGTH_SHORT).show();
                    }
                } else {
                    Toast.makeText(context, "the usbDevice be null", Toast.LENGTH_SHORT).show();
                }
            }
        }
    }
};

/**
 * 筛选出我们想要的usb设备
 *
 * @param usbAccessory
 * @return
 */
public boolean filterDevice(UsbAccessory usbAccessory) {
    // TODO: 2019/3/13 对应判断设备是否是我们要连接的设备
    return false;
}


  



二、USB 设备的连接和使用

2.USB 设备的插入

Android 系统中,USB 设备的插入和拔出是以系统广播的形式发送的,我们只要注册监听这个广播就好

public class USBReceiver extends BroadcastReceiver {
public static final String ACTION_USB_PERMISSION = “com.android.example.USB_PERMISSION”;

@Override
public void onReceive(Context context, Intent intent) {
    String action = intent.getAction();
    if (ACTION_USB_PERMISSION.equals(action)) {
        // 获取权限结果的广播
        synchronized (this) {
            UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
            if (device != null) {
                //call method to set up device communication
                if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                    Log.e("USBReceiver", "获取权限成功:" + device.getDeviceName());
                } else {
                    Log.e("USBReceiver", "获取权限失败:" + device.getDeviceName());
                }
            }
        }
    }else if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) {
        // 有新的设备插入了,在这里一般会判断这个设备是不是我们想要的,是的话就去请求权限
    } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
        // 有设备拔出了
    }
}

}

 
/**
 * 请求获取指定 USB 设备的权限
 */
public void requestPermission(UsbDevice device) {
    if (device != null) {
        if (usbManager.hasPermission(device)) {
            Toast.makeText(context, "已经获取到权限", Toast.LENGTH_SHORT).show();
        } else {
            if (mPermissionIntent != null) {
                usbManager.requestPermission(device, mPermissionIntent);
                Toast.makeText(context, "请求USB权限", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(context, "请注册USB广播", Toast.LENGTH_LONG).show();
            }
        }
    }
}
  

注册广播:
public void registerReceiver(Activity context) {
    mPermissionIntent = PendingIntent.getBroadcast(context, 0, new Intent(ACTION_USB_PERMISSION), 0);
    IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
    filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
    filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
    context.registerReceiver(usbReceiver, filter);
}
   
7.通信

与 USB 设备的通信可以是同步的也可以是异步的。无论哪种情况,你都应该创建一个新线程来执行所有数据传输,避免阻塞UI线程。

第一步,打开通信端口
public boolean openPort(UsbDevice device) {
    //获取设备接口,一般只有一个,多个的自己研究去
    usbInterface = device.getInterface(0);

    // 判断是否有权限
    if (hasPermission(device)) {
        // 打开设备,获取 UsbDeviceConnection 对象,连接设备,用于后面的通讯
        usbConnection = usbManager.openDevice(device);

        if (usbConnection == null) {
            return false;
        }
        if (usbConnection.claimInterface(usbInterface, true)) {
            Toast.makeText(Utils.getContext(), "找到 USB 设备接口", Toast.LENGTH_SHORT).show();
        } else {
            usbConnection.close();
            Toast.makeText(Utils.getContext(), "没有找到 USB 设备接口", Toast.LENGTH_SHORT).show();
            return false;
        }
    } else {
        Toast.makeText(Utils.getContext(), "没有 USB 权限", Toast.LENGTH_SHORT).show();
        return false;
    }

    //获取接口上的两个端点,分别对应 OUT 和 IN
    for (int i = 0; i < usbInterface.getEndpointCount(); ++i) {
        UsbEndpoint end = usbInterface.getEndpoint(i);
        if (end.getDirection() == UsbConstants.USB_DIR_IN) {
            usbEndpointIn = end;
        } else {
            usbEndpointOut = end;
        }
    }
    return true;
}
     
 

第二步,发送数据

usbConnection.bulkTransfer(usbEndpointOut, bytes, bytes.length, 500);





相关 开源库
链接: [usb-serial-for-android](https://github.com/mik3y/usb-serial-for-android).

  • 3
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值