内容提供者ContentProvider和内容解析者ContentResolver

简介

ContentProvider 在android中的作用是对外共享数据,也就是说你可以通过ContentProvider把应用中的数据共享给其他应用访问,其他应用可以通过ContentProvider 对你应用中的数据进行添删改查。关于数据共享,以前我们学习过文件操作模式,知道通过指定文件的操作模式为Context.MODE_WORLD_READABLE 或Context.MODE_WORLD_WRITEABLE同样也可以对外共享数据。那么,这里为何要使用ContentProvider 对外共享数据呢?是这样的,如果采用文件操作模式对外共享数据,数据的访问方式会因数据存储的方式而不同,导致数据的访问方式无法统一,如:采用xml文件对外共享数据,需要进行xml解析才能读取数据;采用sharedpreferences共享数据,需要使用sharedpreferences API读取数据。
使用ContentProvider对外共享数据的好处是统一了数据的访问方式。

ContentProvider的原理是按照一定规则暴露自己的接口给其它应用来访问自己应用的数据(其实就是自定义增删改查接口并暴露出去,让别的应用访问自己的数据)。

ContentResolver就是按照一定规则访问内容提供者的数据(其实就是调用内容提供者自定义的接口来操作它的数据)。

ContentProvider对外共享数据:

步骤

1. 定义一个类 继承 ContentProvider
2. 定义匹配规则 指定主机名 + path  code    urimatcher  content:// 
3. 通过静态代码块添加匹配规则 
4. 一定要记得在清单文件配置内容提供者 不要忘记加authorities 
说明:
第一步继承ContentProvider需要重写下面方法:
public class PersonContentProvider extends ContentProvider{
  public boolean onCreate()
  public Uri insert(Uri uri, ContentValues values)
  public int delete(Uri uri, String selection, String[] selectionArgs)
  public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
  public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
  public String getType(Uri uri)}

第四步需要在AndroidManifest.xml使用<provider>对该ContentProvider进行配置,为了能让其他应用找到该ContentProvider , ContentProvider 

采用了authorities(主机名/域名)对它进行唯一标识,你可以把 ContentProvider看作是一个网站(想想,网站也是提供数据者),authorities 就是他的域名:
<manifest .... >
   <application android:icon="@drawable/icon" android:label="@string/app_name">
       <provider android:name=".PersonContentProvider" android:authorities="cn.itcast.providers.personprovider"/>
   </application>
</manifest>


ContentProvider类主要方法的作用:

public boolean onCreate()
该方法在ContentProvider创建后就会被调用, Android开机后, ContentProvider在其它应用第一次访问它时才会被创建。
public Uri insert(Uri uri, ContentValues values)
该方法用于供外部应用往ContentProvider添加数据。
public int delete(Uri uri, String selection, String[] selectionArgs)
该方法用于供外部应用从ContentProvider删除数据。
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
该方法用于供外部应用更新ContentProvider中的数据。
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
该方法用于供外部应用从ContentProvider中获取数据。
public String getType(Uri uri)
该方法用于返回当前Url所代表数据的MIME类型。如果操作的数据属于集合类型,那么MIME类型字符串应该以vnd.android.cursor.dir/开头,

例如:要得到所有person记录的Uri为content://cn.itcast.provider.personprovider/person,那么返回的MIME类型字符串应该为:“vnd.android.cursor.dir/person”。如果要操作的数据属于非集合类型数据,那么MIME类型字符串应该以vnd.android.cursor.item/开头,例如:得到id为10的person记录,Uri为content://cn.itcast.provider.personprovider/person/10,那么返回的MIME类型字符串应该为:“vnd.android.cursor.item/person”。

Uri介绍

Uri代表了要操作的数据,Uri主要包含了两部分信息:1》需要操作的ContentProvider ,2》对ContentProvider中的什么数据进行操作,一个Uri由以下几部分组成:


ContentProvider(内容提供者)的scheme已经由Android所规定, scheme为:content://
主机名(或叫Authority)用于唯一标识这个ContentProvider,外部调用者可以根据这个标识来找到它。
路径(path)可以用来表示我们要操作的数据,路径的构建应根据业务而定,如下:
要操作person表中id为10的记录,可以构建这样的路径:/person/10
要操作person表中id为10的记录的name字段, person/10/name
要操作person表中的所有记录,可以构建这样的路径:/person
要操作xxx表中的记录,可以构建这样的路径:/xxx
当然要操作的数据不一定来自数据库,也可以是文件、xml或网络等其他存储方式,如下:
要操作xml文件中person节点下的name节点,可以构建这样的路径:/person/name
如果要把一个字符串转换成Uri,可以使用Uri类中的parse()方法,如下:
Uri uri = Uri.parse("content://cn.itcast.provider.personprovider/person")

UriMatcher类使用介绍

因为Uri代表了要操作的数据,所以我们经常需要解析Uri,并从Uri中获取数据。Android系统提供了两个用于操作Uri的工具类,分别为UriMatcher 和ContentUris 。

掌握它们的使用,会便于我们的开发工作。
UriMatcher类用于匹配Uri,它的用法如下:
首先第一步把你需要匹配Uri路径全部给注册上,如下:


//常量UriMatcher.NO_MATCH表示不匹配任何路径的返回码
UriMatcher  sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//如果match()方法匹配content://cn.itcast.provider.personprovider/person路径,返回匹配码为1
sMatcher.addURI(“cn.itcast.provider.personprovider”, “person”, 1);//添加需要匹配uri,如果匹配就会返回匹配码
//如果match()方法匹配content://cn.itcast.provider.personprovider/person/230路径,返回匹配码为2
sMatcher.addURI(“cn.itcast.provider.personprovider”, “person/#”, 2);//#号为通配符
switch (sMatcher.match(Uri.parse("content://cn.itcast.provider.personprovider/person/10"))) { 
  case 1
   break;
  case 2
   break;
  default://不匹配
   break;
}
注册完需要匹配的Uri后,就可以使用sMatcher.match(uri)方法对输入的Uri进行匹配,如果匹配就返回匹配码,匹配码是调用addURI()方法传入的第三个参数,

假设匹配content://cn.itcast.provider.personprovider/person路径,返回的匹配码为1

代码示例

package com.itheima.transaction;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;

public class AccountProvider extends ContentProvider {

	private static final int QUEYSUCESS = 0;  // ctrl + shift + X(变大写)   变小写  + y  
	private static final int INSERTSUCESS = 1;
	
	private static final int UPDATESUCESS  = 2;
	
	private static final int DELSUCESS  = 3;
	
	//1 想使用内容提供者 必须定义 匹配规则   code:定义的匹配规则 如果 匹配不上  有一个返回码  -1
	 static UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);

	private MyOpenHelper helper;
	
	//2 我要添加匹配规则
	 
	 static{
		 //开始添加匹配规则 
		 /**
		  * authority   主机名  通过主机名来访问我暴露的数据 
		  * path   你也可以随意 写 com.itheima.contentprovider/query
		  * code 匹配码 
		  */
		 matcher.addURI("com.itheima.contentprovider", "query", QUEYSUCESS);
		 //添加插入匹配规则
		 matcher.addURI("com.itheima.contentprovider", "insert", INSERTSUCESS);
		 //添加更新匹配规则
		 matcher.addURI("com.itheima.contentprovider", "update", UPDATESUCESS);
		 //添加删除匹配规则
		 matcher.addURI("com.itheima.contentprovider", "delete", DELSUCESS);
		 
		 
	 }
	 
	
	@Override
	public boolean onCreate() {
		helper = new MyOpenHelper(getContext());
		
		
		
		return false;
	}

	//Uri 范围比较大  不但可以指定  tel:   可以定义很多语法 
	@Override
	public Cursor query(Uri uri, String[] projection, String selection,
			String[] selectionArgs, String sortOrder) {
		//传递过来的uri 是否和我们定义的匹配规则 匹配
		int match = matcher.match(uri);
		if (match == QUEYSUCESS ) {
			//说明匹配成功 
			SQLiteDatabase db = helper.getReadableDatabase();  //获取数据库对象 
			Cursor cursor = db.query("info", projection, selection, selectionArgs, null, null, sortOrder);
			
			//注意  这个地方 不要关闭 cursor 和  db 
			
			//大吼一声 数据库发生了改变 
			getContext().getContentResolver().notifyChange(uri, null);
			
			return cursor;
			
		}else{
			//匹配失败 
			throw new IllegalArgumentException("路径匹配失败");
			
		}
		
		
	}

	@Override
	public String getType(Uri uri) {
		return null;
	}

	@Override
	public Uri insert(Uri uri, ContentValues values) {
		
		int match = matcher.match(uri);
		if (match == INSERTSUCESS) {
			//说明匹配成功
			SQLiteDatabase db = helper.getReadableDatabase();
			long insert = db.insert("info", null, values);
			
			//执行上面这句话 说明我的数据库内容 发生了变化   首先 要发送一条通知 说明 我发生改变 
			
			if (insert>0) {

				//数据库发生变化  发送一个通知 
				getContext().getContentResolver().notifyChange(uri, null);
			}
			
			
			
			
			Uri uri2 = Uri.parse("com.itheima.contentprovider/"+insert);
			return uri2;
		}else{
			//匹配失败 
			throw new IllegalArgumentException("路径匹配失败");
			
		}
				
	}

	@Override
	public int delete(Uri uri, String selection, String[] selectionArgs) {
		int match = matcher.match(uri);
		if (match == DELSUCESS) {
			//匹配成功
			SQLiteDatabase db = helper.getReadableDatabase();
			int delete = db.delete("info", selection, selectionArgs);
			
			if (delete>0) {
				//大吼一声 数据库发生了改变 
				getContext().getContentResolver().notifyChange(uri, null);
				
			}
			
			
			return delete;
			
		}else {
			
			//匹配失败 
			throw new IllegalArgumentException("路径匹配失败");
			
		}
		
	}

	@Override
	public int update(Uri uri, ContentValues values, String selection,
			String[] selectionArgs) {
		
		int match = matcher.match(uri);
		if (match == UPDATESUCESS) {
			//匹配成功 
			SQLiteDatabase db = helper.getReadableDatabase();
			int update = db.update("info", values, selection, selectionArgs);
			
			if (update>0) {
				//大吼一声 数据库发生了改变 
				getContext().getContentResolver().notifyChange(uri, null);
			}
			
			return update;
		}else {
			//匹配失败 
			throw new IllegalArgumentException("路径匹配失败");
		}
		
	}

}

内容解析者ContentResolver

使用ContentResolver调用ContentProvider提供的接口,操作数据

当外部应用需要对ContentProvider中的数据进行添加、删除、修改和查询操作时,可以使用ContentResolver 类来完成,要获取ContentResolver 对象,

可以使用Activity提供的getContentResolver()方法。 ContentResolver 类提供了与ContentProvider类相同签名的四个方法:
public Uri insert(Uri uri, ContentValues values)
该方法用于往ContentProvider添加数据。
public int delete(Uri uri, String selection, String[] selectionArgs)
该方法用于从ContentProvider删除数据。
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
该方法用于更新ContentProvider中的数据。
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
该方法用于从ContentProvider中获取数据。


这些方法的第一个参数为Uri,代表要操作的ContentProvider和对其中的什么数据进行操作,假设给定的是: Uri.parse(“content://cn.itcast.providers.personprovider/person/10”),那么将会对主机名为cn.itcast.providers.personprovider的ContentProvider进行操作,操作的数据为person表中id为10的记录。

使用ContentResolver对ContentProvider中的数据进行添加、删除、修改和查询操作:
	ContentResolver resolver =  getContentResolver();
	Uri uri = Uri.parse("content://cn.itcast.provider.personprovider/person");
	//添加一条记录
	ContentValues values = new ContentValues();
	values.put("name", "itcast");
	values.put("age", 25);
	resolver.insert(uri, values);		
	//获取person表中所有记录
	Cursor cursor = resolver.query(uri, null, null, null, "personid desc");
	while(cursor.moveToNext()){
		Log.i("ContentTest", "personid="+ cursor.getInt(0)+ ",name="+ cursor.getString(1));
	}
	//把id为1的记录的name字段值更改新为liming
	ContentValues updateValues = new ContentValues();
	updateValues.put("name", "liming");
	resolver.update(updateIdUri, updateValues, null, null);

监听ContentProvider中数据的变化

如果ContentProvider的访问者需要知道ContentProvider中的数据发生变化,可以在ContentProvider 发生数据变化时调用

getContentResolver().notifyChange(uri, null)来通知注册在此URI上的访问者,例子如下:

public class PersonContentProvider extends ContentProvider {
	public Uri insert(Uri uri, ContentValues values) {
		db.insert("person", "personid", values);
		getContext().getContentResolver().notifyChange(uri, null);
	}
	}
如果ContentProvider的访问者需要得到数据变化通知,必须使用ContentObserver对数据(数据采用uri描述)进行监听,当监听到数据变化通知时,

系统就会调用ContentObserver的onChange()方法:

	getContentResolver().registerContentObserver(Uri.parse("content://cn.itcast.providers.personprovider/person"),
        		true,//true表示只要发出通知的Uri以方法第一个参数开头都能被监听到,否侧监听uri必须与发出通知的uri完全匹配才能被监听到
                                           new PersonObserver(new Handler()));
	public class PersonObserver extends ContentObserver{
	public PersonObserver(Handler handler) {
		super(handler);
	}
	public void onChange(boolean selfChange) {
	    //此处可以进行相应的业务处理
	}
	}

实例代码

利用内容解析者备份短信

package com.itheima.backupsms;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

import org.xmlpull.v1.XmlSerializer;

import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.FileObserver;
import android.app.Activity;
import android.database.Cursor;
import android.util.Xml;
import android.view.Menu;
import android.view.View;

public class MainActivity extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
	}

	/**
	 * 点击按钮备份短信
	 * 
	 * @param v
	 */
	public void backup(View v) {

		try {
			// 1 初始化xml 序列化器
			XmlSerializer serializer = Xml.newSerializer();
			// 2初始化xml序列化器的 参数
			File file = new File(Environment.getExternalStorageDirectory()
					.getPath(), "smsbackup.xml");
			FileOutputStream fos = new FileOutputStream(file);
			serializer.setOutput(fos, "utf-8");

			// 3 开始写 xml 头
			serializer.startDocument("utf-8", true);

			// 开始写 xml 根节点 smss
			serializer.startTag(null, "smss");
			// 1 把我们关系的短信数据库里的内容 给 获取出来 我们要做的操作 就是通过内容解析者 把数据给查询出来
			Uri uri = Uri.parse("content://sms");

			Cursor cursor = getContentResolver().query(uri,
					new String[] { "address", "date", "body" }, null, null,
					null);
			while (cursor.moveToNext()) {
				// 写sms 节点
				serializer.startTag(null, "sms");
				String address = cursor.getString(0);
				String date = cursor.getString(1);
				String body = cursor.getString(2);

				//开始写 address 节点 
				serializer.startTag(null, "address");
				serializer.text(address);
				serializer.endTag(null, "address");
				
				//开始写 date 节点 
				serializer.startTag(null, "date");
				serializer.text(date);
				serializer.endTag(null, "date");
				
				
				//开始写 body 节点 
				serializer.startTag(null, "body");
				serializer.text(body);
				serializer.endTag(null, "body");
				
				serializer.endTag(null, "sms");
			}

			// 开始写 address date body

			serializer.endTag(null, "smss");

			// 文档结束
			serializer.endDocument();

		} catch (Exception e) {
			e.printStackTrace();
		}

	}

}

利用内容解析者插入短信

//1 先拿到 内容解析器 
		Uri uri = Uri.parse("content://sms");
		ContentValues values = new ContentValues();
		values.put("address", "110");
		values.put("date", System.currentTimeMillis());
		values.put("body", "您的事犯了 请您马上来一趟");
		getContentResolver().insert(uri, values);

利用内容解析者获取联系人数据

1. 首先 我要查询raw_contacts表  获取到 contact_id  
2. 去data表  根据 contact_id 去获取 mimetype  data1的数据
3. 然后根据 mimetype_id 来区分数据类型 
package com.itheima.getcontactinfo.utils;

import java.util.ArrayList;
import java.util.List;

import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;

import com.itheima.getcontactinfo.domain.ContactInfo;

public class ContactUtils {

	public static List<ContactInfo> getContactInfos(Context context) {

		List<ContactInfo> contactLists = new ArrayList<ContactInfo>();
		// 1 首先 我要查询raw_contacts表 获取到 contact_id列
		// (1)我如何查询 ?用 内容解析者 path
		Uri uri = Uri.parse("content://com.android.contacts/raw_contacts");
		Uri datauri = Uri.parse("content://com.android.contacts/data");

		ContentResolver resolver = context.getContentResolver();
		Cursor cursor = resolver.query(uri, new String[] { "contact_id" },
				null, null, null);
		while (cursor.moveToNext()) {
			// 获取到contact_id 的值
			String contact_id = cursor.getString(0);
			System.out.println("contact_id--" + contact_id);

			// 有个小细节注意一下 我要判断 contact_id 是否为空
			if (contact_id != null) {

				ContactInfo info = new ContactInfo();
				info.setId(contact_id);

				// 2去data表 根据 contact_id 去获取 mimetyple_id列 data1 列

				Cursor dataCursor = resolver.query(datauri, new String[] {
						"mimetype", "data1" }, "raw_contact_id=?",
						new String[] { contact_id }, null);
				while (dataCursor.moveToNext()) {
					String mimetype = dataCursor.getString(0); // 获取到mimeytype
					String data1 = dataCursor.getString(1);

					// 3 然后根据 mimetype类型 来区分数据类型
					if ("vnd.android.cursor.item/email_v2".equals(mimetype)) {
						info.setEmail(data1);

					} else if ("vnd.android.cursor.item/name".equals(mimetype)) {

						info.setName(data1);

					} else if ("vnd.android.cursor.item/phone_v2"
							.equals(mimetype)) {
						System.out.println("data---电话号码-" + data1);
						info.setPhone(data1);
					}

				}
				dataCursor.close();
				contactLists.add(info); // 把info信息存到集合中

			}

		}
		cursor.close();

		return contactLists;

	}

}
package com.itheima.getcontactinfo.domain;

public class ContactInfo {

	private String id;
	private String name;
	private String phone;
	private String email;
	
	
	
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getPhone() {
		return phone;
	}
	public void setPhone(String phone) {
		this.phone = phone;
	}
	public String getEmail() {
		return email;
	}
	public void setEmail(String email) {
		this.email = email;
	}
	@Override
	public String toString() {
		return "ContactInfo [id=" + id + ", name=" + name + ", phone=" + phone
				+ ", email=" + email + "]";
	}

	
	
	
	
}

利用内容解析者插入联系人

1. 先往raw_contacts 插入一条数据
2. 往data表插入数据 根据mimetype contact_id 往里插入数据 
package com.itheima.insert.contact;

import android.net.Uri;
import android.os.Bundle;
import android.app.Activity;
import android.content.ContentValues;
import android.database.Cursor;
import android.view.Menu;
import android.view.View;
import android.widget.EditText;

public class MainActivity extends Activity {

	private EditText et_name;
	private EditText et_phone;
	private EditText et_email;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		//1找到我们关心的控件
		
		et_name = (EditText) findViewById(R.id.et_name);
		et_phone = (EditText) findViewById(R.id.et_phone);
		et_email = (EditText) findViewById(R.id.et_email);
		
		
		
		
	}
	
	/**
	 * 保存联系人的信息
	 * @param v
	 */
	public void click(View v){
		
		//1 要先拿到  edittext的值 
		String name = et_name.getText().toString().trim();
		String phone = et_phone.getText().toString().trim();
		String email = et_email.getText().toString().trim();
		
		//2 往 raw_contacts里插入一条数据   拿到内容解析者 
		Uri uri = Uri.parse("content://com.android.contacts/raw_contacts");
		Uri datauri = Uri.parse("content://com.android.contacts/data");
		
		ContentValues values = new ContentValues();
		// 插入数据之前  我要先查询 一下  raw_contacts 表一共有多少行的数据 
		Cursor cursor = getContentResolver().query(uri, null, null, null, null);
		int count = cursor.getCount();  //获取到一共有多少行 
	    int contact_id =  count + 1;  
		
		values.put("contact_id", contact_id);
		getContentResolver().insert(uri, values);
		
		// 3 往data 表插入数据 
		
		ContentValues nameValues = new ContentValues();
		nameValues.put("data1", name);
		nameValues.put("mimetype", "vnd.android.cursor.item/name");
		nameValues.put("raw_contact_id", contact_id);
		getContentResolver().insert(datauri, nameValues);

		ContentValues phoneValues = new ContentValues();
		phoneValues.put("data1", phone);
		phoneValues.put("mimetype", "vnd.android.cursor.item/phone_v2");
		phoneValues.put("raw_contact_id", contact_id);
		getContentResolver().insert(datauri, phoneValues);
		
		ContentValues emailValues = new ContentValues();
		emailValues.put("data1", email);
		emailValues.put("mimetype", "vnd.android.cursor.item/email_v2");
		emailValues.put("raw_contact_id", contact_id);
		getContentResolver().insert(datauri, emailValues);
		
		
		
		
		
	}

	
}

利用监听和内容解析者监听短信

package com.itheima.smslistener;

import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.app.Activity;
import android.database.ContentObserver;
import android.database.Cursor;
import android.view.Menu;

public class MainActivity extends Activity {

	private Uri	uri;
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		
		
		//1注册一个内容观察者 
	   	uri = Uri.parse("content://sms");
		getContentResolver().registerContentObserver(uri, true, new MyObserver(new Handler()));
		
	}

	
	
	private  class MyObserver  extends ContentObserver{

		public MyObserver(Handler handler) {
			super(handler);
		}
		@Override
		public void onChange(boolean selfChange) {
			
			//当短信的数据库发生了变化    我就去取出所有短信的内容 
			
			Cursor cursor = getContentResolver().query(uri, new String[]{"address","body","date"}, null, null, null);
			while(cursor.moveToNext()){
				
				String address = cursor.getString(0);
				String body = cursor.getString(1);
				String date = cursor.getString(2);
				
				System.out.println("address---"+address+"--body:"+body);
				
			}
			
			super.onChange(selfChange);
		}
		
		
	}
	
}







  • 6
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Barry__

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值