实现手机扫描二维码页面登录,类似web微信-第三篇,手机客户端

转载自:http://blog.csdn.net/otangba/article/details/8265896

上一篇,介绍了二维码生成的机制,紧接着,我们就要开发手机客户端来识别这个二维码。

二维码,实际上是记录了这个页面的sessionID,目的是为了最后让服务器能通过long polling的机制去通知到这个浏览器。

创建二维码的时候我们采用了nodejs的QRcode库,其实如果换了其他的web服务器,也可以有其他的可选包,例如zxing。

手机上用的比较多的就是zxing库,不过用过的人都知道,zxing库的核心core只是提供二维码的解析,而应用程序本身对摄像头的操作部分必须参考zxing的应用源码。

那个源码比较的复杂,虽然很好理解,但是代码量太大了。如果要分析那部分源码,文章就要写的长篇大论了,所以这一次,我们不用zxing库,而选择一个更为高效实用的android二维码扫描组件:zbar

ZBar不是纯的java代码,而是用了C编译的native library,因此识别的效率上比zxing要高很多。

闲话少说,先看看程序运行的一系列流程吧:

第一步,登录手机软件,我们做测试用,就只需要输入一个用户名,提交到服务器,返回一个token

为什么要做第一步,因为我们实现手机二维码登录的基础原则就是我们的手机客户端必须的登录的,这样才能作为一个凭据

例如微信,假如你不登录是不能扫描的,所以我们的例子模拟一个登录的过程

第二步,登录成功之后,开始扫描,二维码就显示在屏幕上

第三步,扫描完成后,确认是否登录网页

最后,页面提示登录完成

下面开始,由于long polling的过程我已经做好,因此手机软件才能正常运行,而今天我们只说手机客户端,服务器端的内容下一篇再说,所以,我们先假设所有的接口都OK

手机客户端分为三个Activity,分别为登录,扫描,确认

先做第一个activity

eclipse建立项目,为了符合android4的UI规范,我们采用了sherlock actionbar来实现3.x一下版本android系统的actionbar

因此,项目需要引用actionbar lib,sherlock actionbar的库不能直接引用jar包,必须要下载源码并且以lib的方式引用源码

引用完之后,新建一个class,叫做LoginActivity 继承自SherlockActivity

为了要实现在actionbar上的loading进度圈,需要设置窗体的属性

 

[java] view plaincopy
 
  1. requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);  
  2.         setContentView(R.layout.login);  
  3.         setSupportProgressBarIndeterminateVisibility(false);  

 

第一个activity界面很简单,就是几个按钮,但是需要有一次和服务器的通信,也就是登录的过程如果登录成功,则显示下一步扫描的按钮,第一个activity很简单

全部代码:

 

[java] view plaincopy
 
  1. package com.zbiti.qrcodelogin.activity;  
  2.   
  3. import java.util.logging.LogRecord;  
  4.   
  5. import android.content.Context;  
  6. import android.content.Intent;  
  7. import android.os.Bundle;  
  8. import android.os.Handler;  
  9. import android.os.Message;  
  10. import android.view.View;  
  11. import android.view.View.OnClickListener;  
  12. import android.widget.Button;  
  13. import android.widget.EditText;  
  14. import android.widget.TextView;  
  15. import android.widget.Toast;  
  16.   
  17. import com.actionbarsherlock.app.SherlockActivity;  
  18. import com.actionbarsherlock.view.Window;  
  19. import com.zbiti.qrcodelogin.R;  
  20. import com.zbiti.qrcodelogin.util.BaseHttpClient;  
  21.   
  22. public class LoginActivity extends SherlockActivity {  
  23.     private Context mContext;  
  24.     private TextView txtInfo;  
  25.     private EditText txtUserName;  
  26.     private Button btnLogin;  
  27.     private Button btnStartScan;  
  28.     private Button btnRelogin;  
  29.     private String token = null;  
  30.     private final static String LOGIN_URL = "http://192.168.111.109:8000/moblogin?";  
  31.   
  32.     private final static int MSG_LOGIN_FAILED = 0;  
  33.     private final static int MSG_LOGIN_OK = 1;  
  34.   
  35.     @Override  
  36.     protected void onCreate(Bundle savedInstanceState) {  
  37.         super.onCreate(savedInstanceState);  
  38.         mContext = LoginActivity.this;  
  39.         requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);  
  40.         setContentView(R.layout.login);  
  41.         setSupportProgressBarIndeterminateVisibility(false);  
  42.         // reference all used view  
  43.         txtInfo = (TextView) findViewById(R.id.txt_info);  
  44.         txtUserName = (EditText) findViewById(R.id.edit_username);  
  45.         btnLogin = (Button) findViewById(R.id.btn_client_login);  
  46.         btnStartScan = (Button) findViewById(R.id.btn_startscan);  
  47.         btnRelogin = (Button) findViewById(R.id.btn_relogin);  
  48.         btnRelogin.setOnClickListener(new OnClickListener() {  
  49.   
  50.             @Override  
  51.             public void onClick(View v) {  
  52.                 txtInfo.setText(R.string.login_hint);  
  53.                 findViewById(R.id.cont_login).setVisibility(View.VISIBLE);  
  54.                 findViewById(R.id.cont_loggedin).setVisibility(View.GONE);  
  55.   
  56.             }  
  57.         });  
  58.   
  59.         btnLogin.setOnClickListener(new OnClickListener() {  
  60.   
  61.             @Override  
  62.             public void onClick(View v) {  
  63.                 setSupportProgressBarIndeterminateVisibility(true);  
  64.                 new Thread(new Runnable() {  
  65.   
  66.                     @Override  
  67.                     public void run() {  
  68.                         getToken();  
  69.                     }  
  70.                 }).start();  
  71.             }  
  72.         });  
  73.   
  74.         btnStartScan.setOnClickListener(new OnClickListener() {  
  75.   
  76.             @Override  
  77.             public void onClick(View v) {  
  78.                 Intent intent = new Intent();  
  79.                 intent.putExtra("token", token);  
  80.                 intent.setClass(mContext, MainActivity.class);  
  81.                 startActivity(intent);  
  82.   
  83.             }  
  84.         });  
  85.     }  
  86.   
  87.     private void getToken() {  
  88.         String userName = txtUserName.getText().toString().trim();  
  89.         if (!userName.equals("")) {  
  90.             try {  
  91.                 token = BaseHttpClient.httpGet(LOGIN_URL + userName).trim();  
  92.             } catch (Exception e) {  
  93.                 // TODO Auto-generated catch block  
  94.                 e.printStackTrace();  
  95.             }  
  96.             System.out.println(">>>" + token);  
  97.         }  
  98.         if (token == null)  
  99.             return;  
  100.         if (token.equals("")) {  
  101.             handler.sendEmptyMessage(MSG_LOGIN_FAILED);  
  102.         } else {  
  103.             handler.sendEmptyMessage(MSG_LOGIN_OK);  
  104.         }  
  105.     }  
  106.   
  107.     private Handler handler = new Handler() {  
  108.   
  109.         @Override  
  110.         public void handleMessage(Message msg) {  
  111.             if (msg.what == MSG_LOGIN_OK) {  
  112.                 // 成功获得token  
  113.                 setSupportProgressBarIndeterminateVisibility(false);  
  114.   
  115.                 txtInfo.setText(getString(R.string.token_info, token));  
  116.                 findViewById(R.id.cont_login).setVisibility(View.GONE);  
  117.                 findViewById(R.id.cont_loggedin).setVisibility(View.VISIBLE);  
  118.             } else if (msg.what == MSG_LOGIN_FAILED) {  
  119.                 setSupportProgressBarIndeterminateVisibility(false);  
  120.                 Toast.makeText(mContext, R.string.login_failed,  
  121.                         Toast.LENGTH_SHORT).show();  
  122.             }  
  123.         }  
  124.   
  125.     };  
  126. }  


布局文件:

 

 

[html] view plaincopy
 
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     android:orientation="vertical" >  
  6.   
  7.     <LinearLayout  
  8.         android:layout_width="match_parent"  
  9.         android:layout_height="wrap_content"  
  10.         android:gravity="center_vertical"  
  11.         android:orientation="horizontal" >  
  12.   
  13.         <TextView  
  14.             android:id="@+id/txt_info"  
  15.             android:layout_width="match_parent"  
  16.             android:layout_height="wrap_content"  
  17.             android:layout_margin="20dp"  
  18.             android:drawableLeft="@drawable/chat"  
  19.             android:drawablePadding="10dp"  
  20.             android:text="@string/login_hint" />  
  21.     </LinearLayout>  
  22.   
  23.     <LinearLayout  
  24.         android:id="@+id/cont_loggedin"  
  25.         android:layout_width="match_parent"  
  26.         android:layout_height="wrap_content"  
  27.         android:orientation="vertical"  
  28.         android:visibility="gone" >  
  29.   
  30.         <LinearLayout  
  31.             android:layout_width="match_parent"  
  32.             android:layout_height="wrap_content"  
  33.             android:orientation="vertical"  
  34.             android:paddingLeft="20dp"  
  35.             android:paddingRight="20dp"  
  36.             android:paddingTop="10dp" >  
  37.   
  38.             <Button  
  39.                 android:id="@+id/btn_startscan"  
  40.                 android:layout_width="match_parent"  
  41.                 android:layout_height="wrap_content"  
  42.                 android:text="@string/start_scan" >  
  43.             </Button>  
  44.   
  45.             <Button  
  46.                 android:id="@+id/btn_relogin"  
  47.                 android:layout_marginTop="10dp"  
  48.                 android:layout_width="match_parent"  
  49.                 android:layout_height="wrap_content"  
  50.                 android:text="@string/relogin" >  
  51.             </Button>  
  52.         </LinearLayout>  
  53.     </LinearLayout>  
  54.   
  55.     <LinearLayout  
  56.         android:id="@+id/cont_login"  
  57.         android:layout_width="match_parent"  
  58.         android:layout_height="wrap_content"  
  59.         android:orientation="vertical" >  
  60.   
  61.         <LinearLayout  
  62.             android:layout_width="match_parent"  
  63.             android:layout_height="wrap_content"  
  64.             android:orientation="horizontal"  
  65.             android:paddingLeft="20dp"  
  66.             android:paddingRight="20dp"  
  67.             android:paddingTop="10dp" >  
  68.   
  69.             <TextView  
  70.                 android:layout_width="60dp"  
  71.                 android:layout_height="wrap_content"  
  72.                 android:text="@string/user_name" />  
  73.   
  74.             <EditText  
  75.                 android:id="@+id/edit_username"  
  76.                 android:layout_width="match_parent"  
  77.                 android:layout_height="wrap_content" />  
  78.         </LinearLayout>  
  79.   
  80.         <LinearLayout  
  81.             android:layout_width="match_parent"  
  82.             android:layout_height="wrap_content"  
  83.             android:orientation="horizontal"  
  84.             android:paddingLeft="20dp"  
  85.             android:paddingRight="20dp"  
  86.             android:paddingTop="10dp" >  
  87.   
  88.             <Button  
  89.                 android:id="@+id/btn_client_login"  
  90.                 android:layout_width="match_parent"  
  91.                 android:layout_height="wrap_content"  
  92.                 android:text="@string/login" >  
  93.             </Button>  
  94.         </LinearLayout>  
  95.     </LinearLayout>  
  96.   
  97. </LinearLayout>  

 

 

代码里的 privatefinalstatic StringLOGIN_URL ="http://192.168.111.109:8000/moblogin?";

这一行是我本地测试用的模拟验证的服务器地址,和生成二维码的页面一样,都是Nodejs生成的,代码我们下一篇解释,这个接口接收手机填写的用户名,并且通过sha1进行加密,将加密过后的字符串返回给手机,手机将这个字符串作为token变量并且会传递下去。

 

 

下面开始第二个activity,就是扫描界面

首先引用zbar的包,将zbar相关的包拷贝进libs目录

包含的so文件就是c编写的native code

新建类MainActivity继承自SherlockActivity

实现扫描的代码可以从zbar的例子里整,这里不重复

需要把上一个activity传递的token获取,并往下传递

 

[java] view plaincopy
 
  1. @Override  
  2.     public void onCreate(Bundle savedInstanceState) {  
  3.         super.onCreate(savedInstanceState);  
  4.         setContentView(R.layout.main);  
  5.         getSupportActionBar().setDisplayHomeAsUpEnabled(true);  
  6.         autoFocusHandler = new Handler();  
  7.         preview = (FrameLayout) findViewById(R.id.cameraPreview);  
  8.         // mCamera = getCameraInstance();  
  9.         Intent intent = getIntent();  
  10.         token = intent.getStringExtra("token");  
  11.         if (token == null || token.equals(""))  
  12.             finish();  
  13.     }  


在扫描完成的回调里,我们将扫描获得sessionID和token一起往下一个activity传递

 

 

[java] view plaincopy
 
  1. PreviewCallback previewCb = new PreviewCallback() {  
  2.         public void onPreviewFrame(byte[] data, Camera camera) {  
  3.             Camera.Parameters parameters = camera.getParameters();  
  4.             Size size = parameters.getPreviewSize();  
  5.   
  6.             Image barcode = new Image(size.width, size.height, "Y800");  
  7.             barcode.setData(data);  
  8.   
  9.             int result = scanner.scanImage(barcode);  
  10.             String qrcodeString = null;  
  11.             if (result != 0) {  
  12.                 previewing = false;  
  13.                 mCamera.setPreviewCallback(null);  
  14.                 mCamera.stopPreview();  
  15.   
  16.                 SymbolSet syms = scanner.getResults();  
  17.                 for (Symbol sym : syms) {  
  18.                     qrcodeString = sym.getData();  
  19.                 }  
  20.   
  21.             }  
  22.             if (qrcodeString != null) {  
  23.                 Intent intent = new Intent();  
  24.                 intent.setClass(MainActivity.this, ConfirmActivity.class);  
  25.                 intent.putExtra("qrcodestring", qrcodeString);  
  26.                 intent.putExtra("token", token);  
  27.                 startActivity(intent);  
  28.             }  
  29.   
  30.         }  
  31.     };  


在完成扫描的回调里,我们把qrcodestring和token都提交给下一个activity

 

接着,我们来写第三个activity

仍然创建一个类集成sherlockactivity,类名ConfirmActivity

这个activity在启动的时候,也就意味着,扫描成功了,那么就先通知服务器端,扫描成功,页面也会即时展示出扫描成功,等待手机确认登录的信息

接下来,如果点确认登录,则通知服务器确认登录。

因此我们有2个接口

 

private final static StringSCANNED_URL ="http://192.168.111.109:8000/scanned?";

privatefinalstatic StringCONFIRMLOGIN_URL ="http://192.168.111.109:8000/confirmed?";

一个是通知服务器已经成功扫描的http接口,一个是通知服务器确认登录的接口。参数都是sessionID,也就是二维码带的信息,和用户token。

 

[java] view plaincopy
 
  1. package com.zbiti.qrcodelogin.activity;  
  2.   
  3. import android.content.Context;  
  4. import android.content.Intent;  
  5. import android.os.Bundle;  
  6. import android.os.Handler;  
  7. import android.os.Message;  
  8. import android.view.View;  
  9. import android.view.View.OnClickListener;  
  10. import android.widget.Button;  
  11. import android.widget.TextView;  
  12.   
  13. import com.actionbarsherlock.app.SherlockActivity;  
  14. import com.actionbarsherlock.view.MenuItem;  
  15. import com.zbiti.qrcodelogin.R;  
  16. import com.zbiti.qrcodelogin.util.BaseHttpClient;  
  17.   
  18. public class ConfirmActivity extends SherlockActivity {  
  19.     private Context mContext;  
  20.     private String sessionID;  
  21.     private String token;  
  22.     private final static String SCANNED_URL = "http://192.168.111.109:8000/scanned?";  
  23.     private final static String CONFIRMLOGIN_URL = "http://192.168.111.109:8000/confirmed?";  
  24.     private final static int LOGIN_SUCCESS=1;  
  25.     private final static int LOGIN_FAIL=0;  
  26.   
  27.     private Button btnConfirmLogin;  
  28.     private Button btnCancel;  
  29.     private TextView txtInfo;  
  30.   
  31.     @Override  
  32.     protected void onCreate(Bundle savedInstanceState) {  
  33.         super.onCreate(savedInstanceState);  
  34.         mContext = ConfirmActivity.this;  
  35.         setContentView(R.layout.confirm_login);  
  36.         getSupportActionBar().setDisplayHomeAsUpEnabled(true);  
  37.         btnConfirmLogin = (Button) findViewById(R.id.btn_login);  
  38.         btnCancel = (Button) findViewById(R.id.btn_cancel);  
  39.         txtInfo=(TextView)findViewById(R.id.txt_confirm_info);  
  40.   
  41.         btnConfirmLogin.setOnClickListener(new OnClickListener() {  
  42.   
  43.             @Override  
  44.             public void onClick(View v) {  
  45.                 new Thread(new Runnable() {  
  46.   
  47.                     @Override  
  48.                     public void run() {  
  49.                         notifyConfirmed();  
  50.   
  51.                     }  
  52.                 }).start();  
  53.   
  54.             }  
  55.         });  
  56.         btnCancel.setOnClickListener(new OnClickListener() {  
  57.   
  58.             @Override  
  59.             public void onClick(View v) {  
  60.                 finish();  
  61.             }  
  62.         });  
  63.   
  64.         Intent intent = getIntent();  
  65.         sessionID = intent.getStringExtra("qrcodestring");  
  66.         token = intent.getStringExtra("token");  
  67.         if (sessionID == null || sessionID == null) {  
  68.             finish();  
  69.         }  
  70.         new Thread(new Runnable() {  
  71.   
  72.             @Override  
  73.             public void run() {  
  74.                 notifyScanned();  
  75.             }  
  76.         }).start();  
  77.   
  78.     }  
  79.   
  80.     private void notifyConfirmed() {  
  81.         String url = CONFIRMLOGIN_URL + "token=" + token + "&sessionid="  
  82.                 + sessionID;  
  83.         String s = null;  
  84.         try {  
  85.             s = BaseHttpClient.httpGet(url);  
  86.             if(s.equals("confirmed")){  
  87.                 handler.sendEmptyMessage(LOGIN_SUCCESS);  
  88.             }else{  
  89.                 handler.sendEmptyMessage(LOGIN_FAIL);  
  90.             }  
  91.         } catch (Exception e) {  
  92.             e.printStackTrace();  
  93.         }  
  94.   
  95.     }  
  96.   
  97.     private void notifyScanned() {  
  98.         String url = SCANNED_URL + "token=" + token + "&sessionid=" + sessionID;  
  99.         String s = null;  
  100.         try {  
  101.             s = BaseHttpClient.httpGet(url);  
  102.         } catch (Exception e) {  
  103.             e.printStackTrace();  
  104.         }  
  105.   
  106.     }  
  107.     private Handler handler= new Handler(){  
  108.   
  109.         @Override  
  110.         public void handleMessage(Message msg) {  
  111.             if(msg.what==LOGIN_FAIL){  
  112.                 txtInfo.setText(R.string.pc_login_fail);  
  113.             }else if(msg.what==LOGIN_SUCCESS){  
  114.                 btnConfirmLogin.setVisibility(View.GONE);  
  115.                 btnCancel.setVisibility(View.GONE);  
  116.                 txtInfo.setText(R.string.pc_login_succuess);  
  117.             }  
  118.         }  
  119.           
  120.     };  
  121.   
  122.     @Override  
  123.     public boolean onMenuItemSelected(int featureId, MenuItem item) {  
  124.         if (item.getItemId() == android.R.id.home) {  
  125.             finish();  
  126.         }  
  127.         return super.onMenuItemSelected(featureId, item);  
  128.     }  
  129. }  


布局如下:

 

 

[html] view plaincopy
 
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     android:orientation="vertical" >  
  6.   
  7.     <ImageView  
  8.         android:layout_width="wrap_content"  
  9.         android:layout_height="wrap_content"  
  10.         android:layout_gravity="center_horizontal"  
  11.         android:layout_marginBottom="10dp"  
  12.         android:layout_marginLeft="40dp"  
  13.         android:layout_marginRight="40dp"  
  14.         android:layout_marginTop="10dp"  
  15.         android:src="@drawable/pcs" />  
  16.   
  17.     <TextView android:id="@+id/txt_confirm_info"  
  18.         android:layout_width="wrap_content"  
  19.         android:layout_height="wrap_content"  
  20.         android:layout_gravity="center_horizontal"  
  21.         android:text="@string/confirm_login_label"  
  22.         android:textSize="18sp" />  
  23.   
  24.     <Button android:id="@+id/btn_login"  
  25.         android:layout_width="match_parent"  
  26.         android:layout_height="wrap_content"  
  27.         android:layout_marginTop="20dp"  
  28.         android:layout_marginLeft="40dp"  
  29.         android:layout_marginRight="40dp"  
  30.         android:text="@string/btn_confirm_login" />  
  31.   
  32.     <Button android:id="@+id/btn_cancel"  
  33.         android:layout_width="match_parent"  
  34.         android:layout_height="wrap_content"  
  35.         android:layout_marginLeft="40dp"  
  36.         android:layout_marginRight="40dp"  
  37.         android:text="@string/btn_cancel" />  
  38.   
  39. </LinearLayout>  


这样一个手机客户端就完成了,其中用到的http请求的过程如下:

 

 

[java] view plaincopy
 
  1. public static String httpGet(String url) throws Exception{  
  2.         String result = null;  
  3.         HttpClient client = new DefaultHttpClient();   
  4.         HttpGet get = new HttpGet(url);  
  5.         HttpResponse response = client.execute(get);  
  6.         if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {   
  7.             InputStream is = response.getEntity().getContent();  
  8.             result=inStream2String(is);  
  9.         }  
  10.         return result;  
  11.     }  
  12.       
  13.     public static String inStream2String(InputStream is) throws Exception {    
  14.         ByteArrayOutputStream baos = new ByteArrayOutputStream();    
  15.         byte[] buf = new byte[1024];    
  16.         int len = -1;    
  17.         while ((len = is.read(buf)) != -1) {    
  18.             baos.write(buf, 0, len);    
  19.         }    
  20.         return new String(baos.toByteArray());    
  21.     }   

 

string如下

 

[html] view plaincopy
 
  1. <resources>  
  2.     <string name="app_name">二维码登录客户端</string>  
  3.     <string name="confirm_login_title">已经扫描,请确认登录</string>  
  4.     <string name="confirm_login_label">即将在浏览器上登录系统\n请确认是否是本人操作</string>  
  5.     <string name="btn_confirm_login">我确认登录系统</string>  
  6.     <string name="btn_cancel">取消</string>  
  7.     <string name="user_name">用户名</string>  
  8.     <string name="user_password">密 码</string>  
  9.     <string name="login">登录</string>  
  10.     <string name="relogin">重新登录</string>  
  11.     <string name="start_scan">开始扫描</string>  
  12.     <string name="login_failed">登录失败</string>  
  13.     <string name="login_hint">随便输入用户名,登录之后,服务器会返回一个代表你身份的token。</string>  
  14.     <string name="token_info">您已经成功登录,token:\n %1$s</string>  
  15.     <string name="pc_login_succuess">网页登录成功</string>  
  16.     <string name="pc_login_fail">网页登录失败,可能您扫描的页面已过期!</string>  
  17. </resources>  

 

原创文章,转载请注明出处

 

【为了方便测试,我把客户端打包上传,可以点这里下载 http://download.csdn.net/detail/otangba/4857059 】

测试程序在运行时可以设置服务器地址,服务器我们在下一篇会介绍。

 

 

 

实现手机扫描二维码页面登录,类似web微信-第四篇,服务器端
http://www.cnblogs.com/fengyun99/p/3541256.html

实现手机扫描二维码页面登录,类似web微信-第三篇,手机客户端
http://www.cnblogs.com/fengyun99/p/3541254.html

实现手机扫描二维码页面登录,类似web微信-第二篇,关于二维码的自动生成
http://www.cnblogs.com/fengyun99/p/3541251.html

实现手机扫描二维码页面登录,类似web微信-第一篇,业务分析
http://www.cnblogs.com/fengyun99/p/3541249.html

posted @ 2014-02-09 10:19  fengyun99  阅读(2553)  评论(0编辑  收藏  举报