Android C2DM学习 - 客户端代码开发

一、基础知识

前面这章我们介绍了C2DM 云端推送,我们从整体上介绍并实现了下Android C2DM的Push功能,在接下来的部分里,我们先来回顾一下C2DM相关的整体上的知识,然后具体介绍说明实现的过程。

在前面的C2DM框架说明中,我们已经知道,要实现Android的C2DM推送功能,需要涉及到三个功能实体:

1. Android设备:推送消息的接收端,在上面会运行我们的客户端程序
2. 第三方服务器:这是我们自己控制的服务器,推送消息的发送端,利用C2DM服务器发送我们要推送的消息
3. C2DM服务器:这是Google已经实现好的服务器,接收我们服务器的数据并把他们发送给对应的Android设备

这三个功能实体部分,其中C2DM服务器是谷歌开发并且已经实现好的,我们只需按其要求的格式与其进行交互即可。我们自己要开发的为另两个实体部分:Android设备上运行的客户端程序的开发和实现第三方服务器上的功能。

并且在整个完整的C2DM推送过程中,要涉及到一些验证用的信息:

1. Sender ID:这是我们前面说过的在这里注册的账号,这个Sender ID主要用来当Android设备上的客户端程序向C2DM服务器注册的时候验证其有使用C2DM服务的权限。

2. Application ID:使用C2DM功能的完整应用程序名,主要用来确保接收到的Push信息绑定到正确的应用程序。

3. Registration ID:这是Android设备上的客户端程序向C2DM服务器注册成功后返回的ID,然后客户端程序需要把这个ID发送给第三方服务器,然后第三方服务器使用这个ID值来向这个设备推送信息。

4. Google User Account:需要在Android设备上登录的谷歌账户,因为C2DM服务是通过已经建立连接的谷歌后台服务来找到对应消息要推送的设备。这个账户验证信息只要在设备上登陆即可,不需要在客户端程序中出现。

5. Sender Auth Token:这是第一个Sender ID对应的使用C2DM服务的权限,在第三方服务器的程序中向Google申请,并且向C2DM服务器发送要推送的消息时要附带这个信息。

这5个和验证相关的信息中,前4个在Android设备上的客户端程序中都有相关,第三方服务器上的程序要使用第3个和第5个验证信息。

最后我们再从整体上来看下Cloud-To-Device Message的主要处理过程,更概括的话可以分为三个步骤:

1. 使能C2DM功能:第一步为Android设备上的客户端程序向C2DM服务器注册,允许接收C2DM的推送消息。

2. 发送推送消息:第二步为第三方服务器通过C2DM服务器向Android设备发送推送信息。

3.  接收推送信息:第三步为Android设备上的客户端程序接收来自C2DM服务器的推送消息。

其中第一步和第三步是在Android设备上的客户端程序中实现,第二步是在第三方服务器上实现。

我们知道完整的C2DM推送功能要涉及Android设备客户端和第三方服务器两方面程序的开发,下面我们首先来具体学习客户端部分的代码开发。

二、客户端开发说明

客户端要实现两个步骤,使能C2DM功能和接收推送消息。

使能C2DM功能,即客户端程序向Google的C2DM服务器注册C2DM服务,使程序允许接收推送消息,过程包含以下三个步骤:

1. 首先客户端程序需要向C2DM服务器启动注册需要的registration Intent。

这个registration Intent(com.google.android.c2dm.intent.REGISTER)需要包含两个内容信息:一个是Sender ID;另一个是Application ID;即我们上面说到的验证信息的前两个。

2. 如果注册成功,C2DM服务器会广播一个com.google.android.c2dm.intent. REGISTRATION Intent,我们的客户端程序需要响应并接收这个Intent,并且从其中获取注册成功返回的Registration ID。

为了后面的使用,客户端程序需要保存这个Registration ID。因为Google可能不定时更新Registration ID值,并通过REGISTRATION Intent进行告知,因此我们的程序需要能进行对应的响应,获取新的Registration ID值并更新保存。

3. 为了完成注册过程,最后一步是我们的客户端程序需要把获得的Registration ID值发送给我们的第三方服务器,并且一般来说第三方服务器要把Registration ID值保存在数据库中。

客户端程序也可以发送com.google.android.c2dm.intent.UNREGISTER Intent取消注册,从而不再接收C2DM服务器发送的推送信息。

Android设备接收推送信息的过程包含以下三个步骤:

1. Android系统获取C2DM服务器推送过来的信息,并且从信息内容中提取键值对数据。

2. Android系统向对应的客户端程序发送com.google.android.c2dm.intent.RECEIVE Intent并在其中包含键值对数据。

3. 客户端程序响应RECEIVE Intent并从中提取出键值对数据,最后根据之前就和发送数据的第三方服务器端商量好的键值,提取对应的数据。

前面介绍了很多相关知识,接下来我们重新实现一下客户端的代码。

三、实例开发

为了能继续使用之前的Sender ID邮箱及已经注册好的应用程序名等信息,我们还是创建一样名为AndroidC2DMDemo的工程。先删除原来的工程或者把Eclipse切换到另一个Workspace下。

创建AndroidC2DMDemo工程,并且包名为com.ichliebephone.c2dm,Min SDK Version选择8。

为了使用C2DM服务,客户端程序要进行两部分处理,

1. 在Manifest.xml文件中声明和C2DM相关的权限。

2. 在Java代码中实现C2DM相关的功能,如前面说的:

a)和C2DM注册相关的代码
b)接收C2DM服务器推送信息相关的代码

下面我们先来实现Java代码部分。

新建一个类C2DMRegistration,用来实现C2DM注册相关功能。

public class C2DMRegistration {

	// 注册C2DM服务
	public static void register(Context context, String senderID) {
		Intent registrationIntent = new Intent(
				"com.google.android.c2dm.intent.REGISTER");
		registrationIntent.putExtra("app",
				PendingIntent.getBroadcast(context, 0, new Intent(), 0)); // boilerplate
		registrationIntent.putExtra("sender", senderID);
		context.startService(registrationIntent);
	}

	// 取消C2DM服务
	public static void unregister(Context context) {
		Intent unregIntent = new Intent(
				"com.google.android.c2dm.intent.UNREGISTER");
		unregIntent.putExtra("app",
				PendingIntent.getBroadcast(context, 0, new Intent(), 0));
		context.startService(unregIntent);
	}

	// 保存注册成功获得的registration_id值
	static void setRegistraionID(Context context, String registrationId) {
		final SharedPreferences prefs = context.getSharedPreferences(
				"c2dm_preference", Context.MODE_PRIVATE);
		Editor editor = prefs.edit();
		editor.putString("dm_registration", registrationId);
		editor.commit();
	}

	// 获取保存的registration_id值
	public static String getRegistrationID(Context context) {
		final SharedPreferences prefs = context.getSharedPreferences(
				"c2dm_preference", Context.MODE_PRIVATE);
		String registrationId = prefs.getString("dm_registration", "");
		return registrationId;
	}

	// 当取消C2DM服务时,清空之前保存着的registration_id值
	static void clearRegistrationId(Context context) {
		final SharedPreferences prefs = context.getSharedPreferences(
				"c2dm_preference", Context.MODE_PRIVATE);
		Editor editor = prefs.edit();
		editor.putString("dm_registration", "");
		editor.commit();
	}

	// 当SERVICE_NOT_AVAILABLE时,需要回退一定时间后重新启动注册
	// 获取当前的回退等待时间
	static long getBackoff(Context context) {
		final SharedPreferences prefs = context.getSharedPreferences(
				"c2dm_preference", Context.MODE_PRIVATE);
		// 默认回退等待时间为30000微秒
		return prefs.getLong("back_off", 30000);
	}

	// 设置回退等待时间
	static void setBackoff(Context context, long backoff) {
		final SharedPreferences prefs = context.getSharedPreferences(
				"c2dm_preference", Context.MODE_PRIVATE);
		Editor editor = prefs.edit();
		editor.putLong("back_off", backoff);
		editor.commit();
	}
}

这个类主要实现了下向C2DM服务器发起和取消注册,并且本地保存、清除和获取注册成功获得的registration_id值,同时还有一个和重新启动注册相关的回退时间值的设置与获取。

其中注册方法很简单,就是发送一个com.google.android.c2dm.intent.REGISTER的Intent,其中包含两个参数,一个是在C2DM网页上注册的Sender ID邮箱,另一个是程序的ID值。

取消注册的方法就是发送一个带有程序ID值的com.google.android.c2dm.intent.UNREGISTER这样的Intent。

并且使用Perference键值对的方式保存获取的registration_id值。

接着再新建一个类C2DMReceiver,用来处理接收到的C2DM服务器的数据。

客户端程序会接收到C2DM服务器的两种类型数据,并且这两种类型的数据都是通过Intent的方式来处理的。一种类型是向C2DM服务器注册后的回调数据,这时Intent对应的Action为com.google.android.c2dm.intent.REGISTRATION;另一种类型是C2DM正式的推送数据,此时Intent对应的Action为com.google.android.c2dm.intent.RECEIVE。

因为C2DMReceiver主要是用来接收Intent,因此需要扩展自BroadcastReceiver。对应的onReceive方法主要就是判断接收C2DM的两种类型数据:

@Override
public void onReceive(Context context, Intent intent) {
    // TODO Auto-generated method stub
    if(intent.getAction().equals("com.google.android.c2dm.intent.REGISTRATION")){
        //如果是注册返回的Intent
        handleRegistration(context, intent);
    }else if(intent.getAction().equals("com.google.android.c2dm.intent.RECEIVE")){
        //如果是接收到C2DM推送消息的Intent
        handleMessage(context, intent);
    }
}

当是接收到注册返回的Intent时,就调用handleRegistration方法:

// 注册后的回调处理
private void handleRegistration(final Context context, Intent intent){
    //如果是注册成功,则Intent中包含"registration_id"键对应的值
    final String registrationId = intent.getStringExtra("registration_id");
    //如果是注册失败,则Intent中包含"error"键对应的值
    String error = intent.getStringExtra("error");
    //如果是取消注册,则Intent中包含"unregistered"键对应的值
    String removed = intent.getStringExtra("unregistered");
    //打印出接收到的registraton_id,为了调试是查看
    Log.v(TAG, "handleRegistration");
    Log.v(TAG, "dmControl: registrationId = " + registrationId +
            ", error = " + error + ", removed = " + removed);
    //
    if(removed != null){
        //如果存在"unregistered"键对应的值,则表示是取消注册
        onUnregistrated(context);
        return;
    }else if(error != null){
        //如果存在"error"键对应的值,则表示注册失败
        onError(context, error);
        return;
    }else{
        //如果以上两种情况都不存在,则表示注册成功了
        onRegistrated(context, registrationId);
    }
    
}

通过获取注册后返回的Intent中数据,来判断是注册失败(error键值存储的内容不为空)、取消注册(unregistered键值存储的内容不为空)还是注册成功(registration_id键值存储的内容不为空)。

当是取消注册时,调用onUnregistrated方法进行处理:

// 取消注册后的处理函数
private void onUnregistrated(Context context){
    Log.v(TAG, "C2DMReceiver Unregister");
    //取消注册的话,同时也清除保存着的registrationId值
    C2DMRegistration.clearRegistrationId(context);
}

主要就是清除之前保存在本地的registrationId值

当是注册失败时,调用onError方法进行处理:

// 出错的处理函数
private void onError(Context context, String errorId){
    Log.v(TAG, "C2DMReceiver Error with the reason: " + errorId);
    //首先清除保存着的registrationId值
    C2DMRegistration.clearRegistrationId(context);
    //判断出错的原因,一共有六种原因:SERVICE_NOT_AVAILABLE,ACCOUNT_MISSING,AUTHENTICATION_FAILED,
    //TOO_MANY_REGISTRATIONS,INVALID_SENDER,PHONE_REGISTRATION_ERROR
    if("SERVICE_NOT_AVAILABLE".equals(errorId)){
        //只有SERVICE_NOT_AVAILABLE这个是C2DM服务器没有响应的原因,可以等待一段时间后重新发送注册请求
        //其他原因都是属于客户端设备没有处理好的原因
        long backoffTimeMs = C2DMRegistration.getBackoff(context);
        Intent retryIntent = new Intent("com.google.android.c2dm.intent.RETRY");
        PendingIntent retryPIntent = PendingIntent.getBroadcast(context, 
                0 /*requestCode*/, retryIntent, 0 /*flags*/);
        AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        am.set(AlarmManager.ELAPSED_REALTIME,
                backoffTimeMs, retryPIntent);
        // 下一次重试时等待更长的时间
        backoffTimeMs *= 2;
        C2DMRegistration.setBackoff(context, backoffTimeMs);
    }
}

如果是注册失败,可以从Intent中获取”error”键值对应数据,根据这个数据值可以查看失败的原因。失败的原因有:

1. SERVICE_NOT_AVAILABLE:Google的服务器未响应。客户端程序可以等待一段时间重新尝试。

2. ACCOUNT_MISSING:Android设备上缺少Google账户。在Android设备上登录一个Google账户后再重新尝试。

3. INVALID_SENDER:Sender ID邮箱C2DM服务器不认识。这个需要把Sender ID邮箱号在C2DM网页上进行注册。

4. PHONE_REGISTRATION_ERROR:当前Android设备不支持C2DM服务。使用2.2及以上版本Android系统来重新尝试。

除了以上四个常见的原因外还有AUTHENTICATION_FAILED和TOO_MANY_REGISTRATIONS原因。

SERVICE_NOT_AVAILABLE是C2DM服务器临时没有响应,可以在代码中进行重新尝试注册。其他都是设备端的原因,都不是可以在代码中解决的。

注册的回调中除了以上两个结果外,就是注册成功返回registration_id的结果了,调用onRegistrated方法:

// 注册成功后的处理函数
private void onRegistrated(Context context, String registrationId){
    Log.v(TAG, "C2DMReceiver Register with the registrationId = " + registrationId);
    //注册成功的话,保存获取的registrationId值
    C2DMRegistration.setRegistraionID(context, registrationId);
    //然后把registrationId值发送给我们自己的第三方服务器
}

注册成功后可以把registration_id值保存在本地。同时重要的是还要发送给我们自己的第三方服务器。发送的方式可以使用HTTP POST的方式等。当把registration_id值发送给了第三方服务器后,完整的注册过程才算完成,之后第三方服务器就可以使用这个registration_id来给我们的客户端程序推送消息了。

C2DM服务器正式的推送消息也在这个类里进行处理。当接收到推送的消息时,就调用handleMessage方法:

// 接收到C2DM推送信息的回调处理
private void handleMessage(Context context, Intent intent){
    onMessage(context, intent);
}

在这个方法里调用了onMessage回调方法:

// 接收到推送消息后的处理函数
private void onMessage(Context context, Intent intent){
    Log.v(TAG, "C2DMReceiver Message");
    Bundle extras = intent.getExtras();
    if(extras!=null){
        //根据键值,提取对应的消息内容
        String msg = (String)extras.get(AndroidC2DMDemo.MESSAGE_KEY_ONE);
        Log.v(TAG, "The received msg = "+msg);
//			//在标题栏上显示通知
        NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
        Notification notification = new Notification(R.drawable.icon, msg, System.currentTimeMillis());
        PendingIntent contentIntent = PendingIntent.getActivity(context, 0, new Intent(context, AndroidC2DMDemo.class), 0);
        notification.setLatestEventInfo(context, context.getString(R.string.app_name), msg, contentIntent);
        notificationManager.notify(0, notification);			
    }
}

前面说过,Android设备接收推送消息有三个步骤,不过前两步都已经在Android2.2及之后的系统中处理,我们的客户端程序只要处理第三步:即从com.google.android.c2dm.intent.RECEIVE Intent中提取键值对数据。键值是应该和第三方服务器端商量好的。获取键值对应的数据方式很简单。为了方便查看,下面的代码是把获取的数据在通知栏处显示出来。

处理C2DM数据的C2DMReceiver的完整代码为:

public class C2DMReceiver extends BroadcastReceiver {
	private static final String TAG = "C2DMReceiver";

	@Override
	public void onReceive(Context context, Intent intent) {
		// TODO Auto-generated method stub
		if (intent.getAction().equals(
				"com.google.android.c2dm.intent.REGISTRATION")) {
			// 如果是注册返回的Intent
			handleRegistration(context, intent);
		} else if (intent.getAction().equals(
				"com.google.android.c2dm.intent.RECEIVE")) {
			// 如果是接收到C2DM推送消息的Intent
			handleMessage(context, intent);
		}
	}

	// 注册后的回调处理
	private void handleRegistration(final Context context, Intent intent) {
		// 如果是注册成功,则Intent中包含"registration_id"键对应的值
		final String registrationId = intent.getStringExtra("registration_id");
		// 如果是注册失败,则Intent中包含"error"键对应的值
		String error = intent.getStringExtra("error");
		// 如果是取消注册,则Intent中包含"unregistered"键对应的值
		String removed = intent.getStringExtra("unregistered");
		// 打印出接收到的registraton_id,为了调试是查看
		Log.v(TAG, "handleRegistration");
		Log.v(TAG, "dmControl: registrationId = " + registrationId
				+ ", error = " + error + ", removed = " + removed);
		//
		if (removed != null) {
			// 如果存在"unregistered"键对应的值,则表示是取消注册
			onUnregistrated(context);
			return;
		} else if (error != null) {
			// 如果存在"error"键对应的值,则表示注册失败
			onError(context, error);
			return;
		} else {
			// 如果以上两种情况都不存在,则表示注册成功了
			onRegistrated(context, registrationId);
		}

	}

	// 接收到C2DM推送信息的回调处理
	private void handleMessage(Context context, Intent intent) {
		onMessage(context, intent);
	}

	// 和C2DM推送信息相关的具体处理函数
	// 注册成功后的处理函数
	private void onRegistrated(Context context, String registrationId) {
		Log.v(TAG, "C2DMReceiver Register with the registrationId = "
				+ registrationId);
		// 注册成功的话,保存获取的registrationId值
		C2DMRegistration.setRegistraionID(context, registrationId);
		// 然后把registrationId值发送给我们自己的第三方服务器
	}

	// 取消注册后的处理函数
	private void onUnregistrated(Context context) {
		Log.v(TAG, "C2DMReceiver Unregister");
		// 取消注册的话,同时也清除保存着的registrationId值
		C2DMRegistration.clearRegistrationId(context);
	}

	// 出错的处理函数
	private void onError(Context context, String errorId) {
		Log.v(TAG, "C2DMReceiver Error with the reason: " + errorId);
		// 首先清除保存着的registrationId值
		C2DMRegistration.clearRegistrationId(context);
		// 判断出错的原因,一共有六种原因:SERVICE_NOT_AVAILABLE,ACCOUNT_MISSING,AUTHENTICATION_FAILED,
		// TOO_MANY_REGISTRATIONS,INVALID_SENDER,PHONE_REGISTRATION_ERROR
		if ("SERVICE_NOT_AVAILABLE".equals(errorId)) {
			// 只有SERVICE_NOT_AVAILABLE这个是C2DM服务器没有响应的原因,可以等待一段时间后重新发送注册请求
			// 其他原因都是属于客户端设备没有处理好的原因
		}

	}

	// 接收到推送消息后的处理函数
	private void onMessage(Context context, Intent intent) {
		Log.v(TAG, "C2DMReceiver Message");
		Bundle extras = intent.getExtras();
		if (extras != null) {
			// 根据键值,提取对应的消息内容
			String msg = (String) extras.get(AndroidC2DMDemo.MESSAGE_KEY_ONE);
			Log.v(TAG, "The received msg = " + msg);
			// //在标题栏上显示通知
			NotificationManager notificationManager = (NotificationManager) context
					.getSystemService(Context.NOTIFICATION_SERVICE);
			Notification notification = new Notification(R.drawable.icon, msg,
					System.currentTimeMillis());
			PendingIntent contentIntent = PendingIntent.getActivity(context, 0,
					new Intent(context, AndroidC2DMDemo.class), 0);
			notification.setLatestEventInfo(context,
					context.getString(R.string.app_name), msg, contentIntent);
			notificationManager.notify(0, notification);
		}
	}
}

主要就是处理了向C2DM注册后的数据和正式推送数据的接收。在实际使用中,需要添加向我们自己的服务器发送registration_id值。

然后还需要在AndroidC2DMDemo中启动向C2DM服务器注册:

public class AndroidC2DMDemo extends Activity {
	/** Called when the activity is first created. */
	private static final String TAG = "AndroidC2DMDemo";
	public static final String SENDER_ID = "android.c2dm.demo@gmail.com"; // 使用C2DM服务的用户账户
	public static final String MESSAGE_KEY_ONE = "msg"; // 和服务器商量好的接收消息的键值key

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);
		Log.v(TAG, "Start");
		if (C2DMRegistration.getRegistrationID(this).equals("")) {
			// 如果本地没有保存registration_id值,则向C2DM服务器注册
			Log.v(TAG, "Register C2DM");
			C2DMRegistration.register(this, SENDER_ID);
		} else {
			// 否则说明已经向C2DM服务器注册过了
			Log.v(TAG, "C2DM registered");
		}
	}
}

完成了Java代码部分,最后还要在Manifest.xml文件中声明和C2DM相关的权限等信息。

为了使用C2DM特性,Manifest.xml需要包含以下几个部分:

1.  <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />

这个说明程序有注册和接收C2DM消息的权限

2.  <uses-permission android:name="android.permission.INTERNET" />

这个在向第三方服务器发送registration_id值时需要使用

3.  设置和声明一个这样的权限:程序的包名 + ".permission.C2D_MESSAGE,如:
<permission android:name="com.ichliebephone.c2dm.permission.C2D_MESSAGE"
       android:protectionLevel="signature"></permission>
<uses-permission android:name="com.ichliebephone.c2dm.permission.C2D_MESSAGE"/>

这表明只有这个应用才能接收到对应Push的消息及注册时返回的结果。

4.  包含com.google.android.c2dm.intent.RECEIVE 和 com.google.android.c2dm.intent.REGISTRATION这两个Action的接收器Receiver,并且类别设置为程序的包名,同时还需要有com.google.android.c2dm.SEND这个权限,如:

<receiver android:name=".C2DMReceiver"
         android:permission="com.google.android.c2dm.permission.SEND">
    <!-- 可以接收实际的Push数据 -->
    <intent-filter>
        <action android:name="com.google.android.c2dm.intent.RECEIVE" />
        <category android:name="com.ichliebephone.c2dm" />
    </intent-filter>
    <!-- 可以接收注册后返回的registration_id -->
    <intent-filter>
        <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
        <category android:name="com.ichliebephone.c2dm" />
    </intent-filter>
</receiver>

这表明这个程序能给接收到C2DM服务器发送的数据

5.  最后还要设置最小SDK版本为8:<uses-sdk android:minSdkVersion="8" />

完整的Manifest.xml文件为:

完整的工程目录为:

工程目录
图1 工程目录

最后创建带有Google API的Android2.2版本及以上的AVD,启动模拟器,在Accounts & Sync中添加账户,就可以运行程序了。

和前一部分的测试方法类似,运行程序后,会在DDMS中看到获取的registrationId值:

获取的registrationId值
图2 获取的registrationId值

然后使用获取的registrationId值利用curl命令模拟第三方服务器向C2DM服务器发送要推送的信息:

使用curl向C2DM服务器发送推送信息
图3 使用curl向C2DM服务器发送推送信息

然后一会我们就可以在DDMS中看到客户端程序收到的推送数据:

获取到的推送数据
图4 获取到的推送数据

同时Android模拟器中也会在通知栏上显示接收到的推送数据:

模拟器接收到的推送数据
图5 模拟器接收到的推送数据

通过测试结果可知,我们实现了Android的C2DM推送功能。

四. 总结

以上我们简单介绍了Android的C2DM推送功能实现过程中,在Android设备上的客户端部分需要实现的内容,及实际的实现过程。不过因为只是用来说明实现过程,因此代码写的尽量简单,并且为了更容易查看,把各种常量字符串等也直接写在代码中了。如果在实际使用中,可以参考Google的C2DM例子Chrome To Phone中的代码,只要包含其com.google.android.c2dm包中的三个文件,并且新建一个扩展C2DMBaseReceiver的子类来处理注册消息和推送消息的回调即可,其代码更加茁壮。不过通过以上的介绍说明,我们应该可以更好的理解C2DM客户端部分的实现了。

以后我们继续学习下C2DM服务器部分的实现。

文章对应的完整代码例子下载地址:AndroidC2DMDemo2.rar

暂无评论
  • 1:请一针见血的评论。
  • 2:评论需要审核通过后才能显示。
  • 3:评论字数限制在1000字以内。
  • 当前字数:0
热门文章
推荐文章
随机文章
关于本站 - 广告服务 - 版权声明 - 联系我们 - 友情链接 - 网站地图 - 帮助中心