资讯专栏INFORMATION COLUMN

android悬浮窗语音识别demo

wangzy2019 / 1804人阅读

摘要:本例用的是语音识别,语义理解引擎,支持强大的用户自定义语义,能更好的解决语义理解。代表返回的行为动作,此处可以看到是就是要求播放,中的数据表示歌曲名称是三国演义。

带有android悬浮窗的语音识别语义理解demo

如发现代码排版问题,请访问CSDN博客

转载请注明CSDN博文地址:http://blog.csdn.net/ls0609/a...

在线听书demo:http://blog.csdn.net/ls0609/a...

语音记账demo:http://blog.csdn.net/ls0609/a...

Android桌面悬浮窗实现比较简单,本篇以一个语音识别,语义理解的demo来演示如何实现android悬浮窗。
1.悬浮窗效果

桌面上待机的时候,悬浮窗吸附在边上

拖动远离屏幕边缘时图标变大,松开自动跑到屏幕边缘,距离屏幕左右边缘靠近哪边吸附哪边

点击悬浮图标时,启动录音

说完后可以点击左button,上传录音给服务器等待处理返回结果

服务器返回结果后自动跳转到应用界面,本例用的是在线听书,跳转到在线听书的界面

2.FloatViewIdle与FloatViewIdleService

1.FloatViewIdle

定义一个FloatViewIdle类,如下是该类的单例模式

public static synchronized FloatViewIdle getInstance(Context context)
{

</>复制代码

  1. if(floatViewManager == null)
  2. {
  3. mContext = context.getApplicationContext();;
  4. winManager = (WindowManager)
  5. mContext.getSystemService(Context.WINDOW_SERVICE);
  6. displayWidth = winManager.getDefaultDisplay().getWidth();
  7. displayHeight = winManager.getDefaultDisplay().getHeight();
  8. floatViewManager = new FloatViewIdle();
  9. }
  10. return floatViewManager;

}

利用winManager 的addview方法,把自定义的floatview添加到屏幕中,那么就会在任何界面显示该floatview,然后再屏蔽非待机界面隐藏floatview,这样就只有待机显示悬浮窗了。
定义两个自定义view,分别是FloatIconView和FloatRecordView,前者就是待机看到的小icon图标,后者是点击这个icon图标后展示的录音的那个界面。
下面来看下怎么定义的FloatIconView
class FloatIconView extends LinearLayout{

</>复制代码

  1. private int mWidth;
  2. private int mHeight;
  3. private int preX;
  4. private int preY;
  5. private int x;
  6. private int y;
  7. public boolean isMove;
  8. public boolean isMoveToEdge;
  9. private FloatViewIdle manager;
  10. public ImageView imgv_icon_left;
  11. public ImageView imgv_icon_center;
  12. public ImageView imgv_icon_right;
  13. public int mWidthSide;
  14. public FloatIconView(Context context) {
  15. super(context);
  16. View view = LayoutInflater.from(mContext).
  17. inflate(R.layout.layout_floatview_icon, this);
  18. LinearLayout layout_content =
  19. (LinearLayout) view.findViewById(R.id.layout_content);
  20. imgv_icon_left = (ImageView) view.findViewById(R.id.imgv_icon_left);
  21. imgv_icon_center = (ImageView) view.findViewById(R.id.imgv_icon_center);
  22. imgv_icon_right = (ImageView) view.findViewById(R.id.imgv_icon_right);
  23. imgv_icon_left.setVisibility(View.GONE);
  24. imgv_icon_center.setVisibility(View.GONE);
  25. mWidth = layout_content.getWidth();
  26. mHeight = layout_content.getHeight();
  27. if((mWidth == 0)||(mHeight == 0))
  28. {
  29. int temp = DensityUtil.dip2px(mContext, icon_width);
  30. mHeight = temp;
  31. icon_width_side_temp = DensityUtil.dip2px(mContext, icon_width_side);
  32. mWidth = icon_width_side_temp;
  33. }
  34. manager = FloatViewIdle.getInstance(mContext);
  35. if(params != null)
  36. {
  37. params.x = displayWidth - icon_width_side_temp;
  38. params.y = displayHeight/2;
  39. }
  40. }
  41. public int getFloatViewWidth()
  42. {
  43. return mWidth;
  44. }
  45. public int getFloatViewHeight()
  46. {
  47. return mHeight;
  48. }
  49. @Override
  50. public boolean onTouchEvent(MotionEvent event)
  51. {
  52. switch(event.getAction())
  53. {
  54. case MotionEvent.ACTION_DOWN:
  55. preX = (int)event.getRawX();
  56. preY = (int)event.getRawY();
  57. isMove = false;
  58. if(params.width == icon_width_side_temp)
  59. handler.sendMessage(handler.obtainMessage(
  60. MSG_UPDATE_FLOAT_VIEW_AFTER_CHANGED, 3, 0));
  61. break;
  62. case MotionEvent.ACTION_UP:
  63. if(isMoveToEdge == true)
  64. {
  65. if(params.width == icon_width_side_temp)
  66. handler.sendMessage(handler.obtainMessage(
  67. MSG_UPDATE_FLOAT_VIEW_AFTER_CHANGED, 3, 0));
  68. handler.sendMessage(handler.obtainMessage(
  69. MSG_FLOAT_VIEW_MOVE_TO_EDGE,this));
  70. }
  71. break;
  72. case MotionEvent.ACTION_MOVE:
  73. x = (int)event.getRawX();
  74. y = (int)event.getRawY();
  75. if(Math.abs(x-preX)>1||Math.abs(y-preY)>1)
  76. {
  77. isMoveToEdge = true;
  78. }
  79. if(Math.abs(x-preX)>5||Math.abs(y-preY)>5)
  80. isMove = true;
  81. if(params.width == icon_width_side_temp)
  82. handler.sendMessage(handler.obtainMessage(
  83. MSG_UPDATE_FLOAT_VIEW_AFTER_CHANGED, 3, 0));
  84. manager.move(this, x-preX, y-preY);
  85. preX = x;
  86. preY = y;
  87. break;
  88. }
  89. return super.onTouchEvent(event);
  90. }

}

通过layout文件生成一个FloatIconView,在onTouchEvent函数中当按下的时候,发送消息更新悬浮view,抬起即up事件时先更新悬浮view,然后再显示吸附到边上的动画。 当move的时候,判断每次位移至少5和像素则更新view位置,这样不断move不断更新就会形成连续的画面。

另一个FloatRecordView(录音的悬浮窗)道理相同,这里就不贴代码了,有兴趣可以下载demo自己编译跑一下。
在FloatIconView中定义一个handler,用于接收消息处理悬浮窗更新位置和吸附的动画

private void initHandler(){

</>复制代码

  1. handler = new Handler(){
  2. @Override
  3. public void handleMessage(Message msg)
  4. {
  5. switch (msg.what)
  6. {
  7. case MSG_REFRESH_VOLUME:
  8. if(floatRecordView != null)
  9. floatRecordView.updateVolume((int)msg.arg1);
  10. break;
  11. case MSG_FLOAT_VIEW_MOVE_TO_EDGE:
  12. //更新悬浮窗位置的动画
  13. moveAnimation((View)msg.obj);
  14. break;
  15. case MSG_REMOVE_FLOAT_VIEW:
  16. if(msg.arg1 == 1)
  17. {//此时已有floatview是floatIconView
  18. if(floatIconView != null)
  19. {//先移除一个floatview
  20. winManager.removeView(floatIconView);
  21. floatIconView = null;
  22. floatRecordView = getFloatRecordView();
  23. if(floatRecordView != null)
  24. {
  25. if(floatRecordView.getParent() == null)
  26. {//再加入一个新的floatview
  27. winManager.addView(floatRecordView, params);
  28. floatViewType = FLOAT_RECORD_VIEW_TYPE;
  29. }
  30. if(mHandler != null)
  31. {
  32. mHandler.sendMessage(mHandler.obtainMessage(
  33. MessageConst.CLIENT_ACTION_START_CAPTURE));
  34. IS_RECORD_FROM_FLOAT_VIEW_IDLE = true;
  35. }
  36. }
  37. }
  38. }
  39. else
  40. {//此时已有floatview是floatRecordView即录音的floatview
  41. if(floatRecordView != null)
  42. {//先移除一个floatview
  43. winManager.removeView(floatRecordView);
  44. floatRecordView = null;
  45. }
  46. floatIconView = getFloatIconView();
  47. if(floatIconView != null)
  48. {
  49. if(floatIconView.getParent() == null)
  50. {/再加入一个新的floatview
  51. winManager.addView(floatIconView, params);
  52. floatViewType = FLOAT_ICON_VIEW_TYPE;
  53. setViewOnClickListener(floatIconView);
  54. }
  55. //可能需要有吸附动画
  56. moveAnimation(floatIconView);
  57. }
  58. }
  59. break;
  60. case MSG_UPDATE_VIEW_SENDING_TO_SERVER:
  61. if(floatRecordView != null)
  62. {
  63. floatRecordView.updateSendingToServerView();
  64. floatRecordView.setTitle("努力识别中");
  65. }
  66. break;
  67. case MSG_UPDATE_ROTATE_VIEW:
  68. if(floatRecordView != null)
  69. {
  70. floatRecordView.rotateview.startRotate();
  71. }
  72. break;
  73. case MSG_UPDATE_FLOAT_VIEW_AFTER_CHANGED:
  74. //1,2是吸附到左边还是右边,3是拖动到中间显示放大的悬浮窗icon
  75. if(msg.arg1 == 1)
  76. changeFloatIconToSide(false);
  77. else if(msg.arg1 == 2)
  78. changeFloatIconToSide(true);
  79. else if(msg.arg1 == 3)
  80. changeFloatIconToNormal();
  81. break;
  82. case MSG_UPDATE_FLOAT_VIEW_ON_SIDE:
  83. if(msg.arg1 == 1)
  84. updateFloatIconOnSide(true);
  85. else if(msg.arg1 == 2)
  86. updateFloatIconOnSide(false);
  87. break;
  88. case MSG_START_ACTIVITY:
  89. hide();
  90. Intent intent = new Intent(mContext,MusicActivity.class);
  91. intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  92. intent.putExtra(START_FROM_FLOAT_VIEW, true);
  93. IS_START_FROM_FLOAT_VIEW_IDLE = true;
  94. mContext.startActivity(intent);
  95. break;
  96. }
  97. }
  98. };

}

那么,怎样做到点击吸附屏幕边缘的悬浮按钮,切换成录音的悬浮窗呢?
public void show()
{

</>复制代码

  1. isHide = false;
  2. floatIconView = getFloatIconView();
  3. if(floatIconView != null)
  4. {
  5. if(floatIconView.getParent() == null)
  6. {
  7. winManager.addView(floatIconView, params);
  8. floatViewType = FLOAT_ICON_VIEW_TYPE;
  9. }
  10. if(floatRecordView != null)
  11. {
  12. handler.sendMessage(handler.obtainMessage(
  13. MSG_REMOVE_FLOAT_VIEW, 2, 0));
  14. }
  15. floatIconView.setOnClickListener(new OnClickListener(){
  16. @Override
  17. public void onClick(View v) {
  18. if(floatIconView.isMove || floatIconView.isMoveToEdge)
  19. {
  20. floatIconView.isMove = false;
  21. return;
  22. }
  23. winManager.removeView(floatIconView);
  24. floatIconView = null;
  25. floatRecordView = getFloatRecordView();
  26. if(floatRecordView != null)
  27. {
  28. if(floatRecordView.getParent() == null)
  29. {
  30. winManager.addView(floatRecordView, params);
  31. floatViewType = FLOAT_RECORD_VIEW_TYPE;
  32. }
  33. if(mHandler != null)
  34. {
  35. mHandler.sendMessage(mHandler.obtainMessage(
  36. MessageConst.CLIENT_ACTION_START_CAPTURE));
  37. IS_RECORD_FROM_FLOAT_VIEW_IDLE = true;
  38. }
  39. }
  40. }
  41. });
  42. }

}

在show函数中,设置了floatIconView的点击事件,移除小的悬浮吸附按钮,加入录音的悬浮窗view并启动录音。

2.FloatViewIdleService

为什么要定义这个service?

这个service用途是,定时扫描是否在待机桌面,如果是待机桌面则显示floatview,否则隐藏。

public class FloatViewIdleService extends Service {

</>复制代码

  1. private static Handler mHandler;
  2. private FloatViewIdle floatViewIdle;
  3. private final static int REFRESH_FLOAT_VIEW = 1;
  4. private boolean is_vertical = true;
  5. @Override
  6. public void onCreate() {
  7. super.onCreate();
  8. initHandler();
  9. }
  10. @Override
  11. public int onStartCommand(Intent intent, int flags, int startId) {
  12. mHandler.sendMessageDelayed(mHandler.obtainMessage(REFRESH_FLOAT_VIEW), 500);
  13. FloatViewIdle.IS_START_FROM_FLOAT_VIEW_IDLE = false;
  14. is_vertical = true;
  15. return START_STICKY;
  16. }
  17. protected void initHandler() {
  18. mHandler = new Handler() {
  19. @Override
  20. public void handleMessage(Message msg) {
  21. switch (msg.what) {
  22. case REFRESH_FLOAT_VIEW://1s发送一次更新floatview消息
  23. updateFloatView();
  24. mHandler.sendMessageDelayed(
  25. mHandler.obtainMessage(REFRESH_FLOAT_VIEW), 1000);
  26. break;
  27. }
  28. }
  29. };
  30. }
  31. private void updateFloatView()
  32. {
  33. boolean isOnIdle = isHome();//判断是否在待机界面
  34. floatViewIdle = FloatViewIdle.getInstance(FloatViewIdleService.this);
  35. if(isOnIdle)
  36. { //待机界面则显示floatview
  37. if(floatViewIdle.getFloatViewType() == 0)
  38. {
  39. floatViewIdle.show();
  40. }
  41. else if(floatViewIdle.getFloatViewType() ==
  42. floatViewIdle.FLOAT_ICON_VIEW_TYPE||
  43. floatViewIdle.getFloatViewType() ==
  44. floatViewIdle.FLOAT_RECORD_VIEW_TYPE)
  45. {
  46. if(this.getResources().getConfiguration().orientation ==
  47. Configuration.ORIENTATION_LANDSCAPE)
  48. {
  49. if(is_vertical == true)
  50. {
  51. floatViewIdle.swapWidthAndHeight();
  52. is_vertical = false;
  53. }
  54. }
  55. else if(this.getResources().getConfiguration().orientation ==
  56. Configuration.ORIENTATION_PORTRAIT)
  57. {
  58. if(is_vertical == false)
  59. {
  60. floatViewIdle.swapWidthAndHeight();
  61. is_vertical = true;
  62. }
  63. }
  64. }
  65. }
  66. else
  67. {//否则隐藏floatview
  68. floatViewIdle.hide();
  69. }
  70. }
  71. private boolean isHome()
  72. {
  73. ActivityManager mActivityManager = (ActivityManager)
  74. getSystemService(Context.ACTIVITY_SERVICE);
  75. List rti = mActivityManager.getRunningTasks(1);
  76. try{
  77. if(rti.size() == 0)
  78. {
  79. return true;
  80. }else
  81. {
  82. if(rti.get(0).topActivity.getPackageName().
  83. equals("com.olami.floatviewdemo"))
  84. return false;
  85. else
  86. return getHomes().contains(rti.get(0).topActivity.getPackageName());
  87. }
  88. }
  89. catch(Exception e)
  90. {
  91. return true;
  92. }
  93. }
  94. private List getHomes()
  95. {
  96. List names = new ArrayList();
  97. PackageManager packageManager = this.getPackageManager();
  98. Intent intent = new Intent(Intent.ACTION_MAIN);
  99. intent.addCategory(Intent.CATEGORY_HOME);
  100. List resolveInfo = packageManager.queryIntentActivities(intent,
  101. PackageManager.MATCH_DEFAULT_ONLY);
  102. for (ResolveInfo ri : resolveInfo) {
  103. names.add(ri.activityInfo.packageName);
  104. }
  105. return names;
  106. }
  107. @Override
  108. public void onDestroy() {
  109. super.onDestroy();
  110. if(floatViewIdle != null)
  111. floatViewIdle.setFloatViewType(0);
  112. }
  113. @Override
  114. public IBinder onBind(Intent intent) {
  115. return null;
  116. }

}

3.启动语音识别

在另一个VoiceSdkService(另一个处理录音服务业务的service)中,当接收到悬浮窗按钮点击事件消息时,则启动录音服务,录音结束后会在onResult回调中收到服务器返回的结果。

本例用的是olami语音识别,语义理解引擎,olami支持强大的用户自定义语义,能更好的解决语义理解。
比如同义理解的时候,我要听三国演义,我想听三国演义,听三国演义这本书,类似的说法有很多,olmai就可以为你解决这类的语义理解,olami语音识别引擎使用比较简单,只需要简单的初始化,然后设置好回调listener,在回调的时候处理服务器返回的json字符串即可,当然语义还是要用户自己定义的。

public void init()
{

</>复制代码

  1. initHandler();
  2. mOlamiVoiceRecognizer = new OlamiVoiceRecognizer(VoiceSdkService.this);
  3. TelephonyManager telephonyManager=(TelephonyManager) this.getSystemService(
  4. (this.getBaseContext().TELEPHONY_SERVICE);
  5. String imei=telephonyManager.getDeviceId();
  6. mOlamiVoiceRecognizer.init(imei);//设置身份标识,可以填null
  7. mOlamiVoiceRecognizer.setListener(mOlamiVoiceRecognizerListener);//设置识别结果回调listener
  8. mOlamiVoiceRecognizer.setLocalization(
  9. OlamiVoiceRecognizer.LANGUAGE_SIMPLIFIED_CHINESE);//设置支持的语音类型,优先选择中文简体
  10. mOlamiVoiceRecognizer.setAuthorization("51a4bb56ba954655a4fc834bfdc46af1",
  11. "asr","68bff251789b426896e70e888f919a6d","nli");
  12. //注册Appkey,在olami官网注册应用后生成的appkey
  13. //注册api,请直接填写“asr”,标识语音识别类型
  14. //注册secret,在olami官网注册应用后生成的secret
  15. //注册seq ,请填写“nli”
  16. mOlamiVoiceRecognizer.setVADTailTimeout(2000);//录音时尾音结束时间,建议填//2000ms
  17. //设置经纬度信息,不愿上传位置信息,可以填0
  18. mOlamiVoiceRecognizer.setLatitudeAndLongitude(31.155364678184498,121.34882432933009);

在VoiceSdkService中定义OlamiVoiceRecognizerListener用于处理录音时的回调
onError(int errCode)//出错回调,可以对比官方文档错误码看是什么错误
onEndOfSpeech()//录音结束
onBeginningOfSpeech()//录音开始
onResult(String result, int type)//result是识别结果JSON字符串
onCancel()//取消识别,不会再返回识别结果
onUpdateVolume(int volume)//录音时的音量,1-12个级别大小音量

本文用的是在线听书的例子,当收到服务器返回的消息是,进入如下函数:

在下面的函数中,通过解析服务器返回的json字符串,提取用户需要的语义理解字段进行处理

private void processServiceMessage(String message)

</>复制代码

  1. {
  2. String input = null;
  3. String serverMessage = null;
  4. try{
  5. JSONObject jsonObject = new JSONObject(message);
  6. JSONArray jArrayNli = jsonObject.optJSONObject("data").optJSONArray("nli");
  7. JSONObject jObj = jArrayNli.optJSONObject(0);
  8. JSONArray jArraySemantic = null;
  9. if(message.contains("semantic"))
  10. jArraySemantic = jObj.getJSONArray("semantic");
  11. else{
  12. input = jsonObject.optJSONObject("data").optJSONObject("asr").
  13. optString("result");
  14. sendMessageToActivity(MessageConst.
  15. CLIENT_ACTION_UPDATA_INPUT_TEXT, 0, 0, null, input);
  16. serverMessage = jObj.optJSONObject("desc_obj").opt("result").toString();
  17. sendMessageToActivity(MessageConst.
  18. CLIENT_ACTION_UPDATA_SERVER_MESSAGE, 0, 0, null, serverMessage);
  19. return;
  20. }
  21. JSONObject jObjSemantic;
  22. JSONArray jArraySlots;
  23. JSONArray jArrayModifier;
  24. String type = null;
  25. String songName = null;
  26. String singer = null;

</>复制代码

  1. if(jObj != null) {
  2. type = jObj.optString("type");
  3. if("musiccontrol".equals(type))
  4. {
  5. jObjSemantic = jArraySemantic.optJSONObject(0);
  6. input = jObjSemantic.optString("input");
  7. jArraySlots = jObjSemantic.optJSONArray("slots");
  8. jArrayModifier = jObjSemantic.optJSONArray("modifier");
  9. String modifier = (String)jArrayModifier.opt(0);
  10. if((jArrayModifier != null) && ("play".equals(modifier)))
  11. {
  12. if(jArraySlots != null)
  13. for(int i=0,k=jArraySlots.length(); i
  14. }

  15. 以我要听三国演义这句语音,服务器返回的数据如下:

  16. {

  17. </>复制代码

    1. "data": {
    2. "asr": {
    3. "result": "我要听三国演义",
    4. "speech_status": 0,
    5. "final": true,
    6. "status": 0
    7. },
    8. "nli": [
    9. {
    10. "desc_obj": {
    11. "result": "正在努力搜索中,请稍等",
    12. "status": 0
    13. },
    14. "semantic": [
    15. {
    16. "app": "musiccontrol",
    17. "input": "我要听三国演义",
    18. "slots": [
    19. {
    20. "name": "songname",
    21. "value": "三国演义"
    22. }
    23. ],
    24. "modifier": [
    25. "play"
    26. ],
    27. "customer": "58df512384ae11f0bb7b487e"
    28. }
    29. ],
    30. "type": "musiccontrol"
    31. }
    32. ]
    33. },
    34. "status": "ok"
  18. }

  19. 1)解析出nli中type类型是musiccontrol,这是语法返回app的类型,而这个在线听书的demo只关心musiccontrol这 个app类型,其他的忽略。
    2)用户说的话转成文字是在asr中的result中获取
    3)在nli中的semantic中,input值是用户说的话,同asr中的result。
    modifier代表返回的行为动作,此处可以看到是play就是要求播放,slots中的数据表示歌曲名称是三国演义。
    那么动作是play,内容是歌曲名称是三国演义,在这个demo中调用
    mBookUtil.searchBookAndPlay(songName,0,0);会先查询,查询到结果会再发播放消息要求播放,我要听三国演义这个流程就走完了。

  20. 关于在线听书请看博文:http://blog.csdn.net/ls0609/a...

  21. 4.源码下载链接

  22. http://pan.baidu.com/s/1o8OELdC

  23. 5.相关链接

  24. 语音记账demo:http://blog.csdn.net/ls0609/a...

  25. olami开放平台语法编写简介:http://blog.csdn.net/ls0609/a...

  26. olami开放平台语法官方介绍:https://cn.olami.ai/wiki/?mp=...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/67617.html

相关文章

  • 记数独X--Android openCV识别数独并自动求解填充APP开发过程

    摘要:可以针对笔者常用的数独本文的实现都基于该,实现数独的识别求解并把答案自动填入。专家级别的平均秒完成求解包括图像数字提取,识别过程,完成全部操作。步骤四数独求解,生成答案,并生成需要填充的数字序列。 1 序   数独是源自18世纪瑞士的一种数学游戏。是一种运用纸、笔进行演算的逻辑游戏。玩家需要根据9×9盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行、每一列、每一个粗线宫(3*3...

    yvonne 评论0 收藏0
  • 大厂黑科技 - 收藏集 - 掘金

    摘要:模仿的功能掘金本模仿了的功能。国内曾经出现的团购类网站有多家,到四年多以后的现在,美团已经是成为国内最大的本地生活服务平台,不管怎饿了么移动的架构演进掘金引言时代演进,技术也随之发展。 模仿 Smartisan OS 的 BigBang 功能 ??? - Android - 掘金 本 Demo 模仿了 Smartisan OS 的 BigBang 功能。App 打开会从剪切板读取文字并...

    fanux 评论0 收藏0
  • Android Q 兼容那些事

    摘要:会议主要是加深开发者对的了解,从而帮助开发者做好的兼容工作。因此本篇我会选择性说明一些在上你需要兼容的一些事情。那么现在有哪些会用到这种呢举一个大家熟悉的。另外目前可以通过在清单文件设置是否启用。因此强烈建议将这个工作排上兼容行程。showImg(https://user-gold-cdn.xitu.io/2019/5/27/16af6c9ce913040d); 5 月 20 号参加了 An...

    roland_reed 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<