1. 程式人生 > >實現手機掃描二維碼頁面登入,類似web微信-第三篇,手機客戶端

實現手機掃描二維碼頁面登入,類似web微信-第三篇,手機客戶端

上一篇,介紹了二維碼生成的機制,緊接著,我們就要開發手機客戶端來識別這個二維碼。

二維碼,實際上是記錄了這個頁面的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進度圈,需要設定窗體的屬性

requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
		setContentView(R.layout.login);
		setSupportProgressBarIndeterminateVisibility(false);

第一個activity介面很簡單,就是幾個按鈕,但是需要有一次和伺服器的通訊,也就是登入的過程如果登入成功,則顯示下一步掃描的按鈕,第一個activity很簡單

全部程式碼:

package com.zbiti.qrcodelogin.activity;

import java.util.logging.LogRecord;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import com.actionbarsherlock.app.SherlockActivity;
import com.actionbarsherlock.view.Window;
import com.zbiti.qrcodelogin.R;
import com.zbiti.qrcodelogin.util.BaseHttpClient;

public class LoginActivity extends SherlockActivity {
	private Context mContext;
	private TextView txtInfo;
	private EditText txtUserName;
	private Button btnLogin;
	private Button btnStartScan;
	private Button btnRelogin;
	private String token = null;
	private final static String LOGIN_URL = "http://192.168.111.109:8000/moblogin?";

	private final static int MSG_LOGIN_FAILED = 0;
	private final static int MSG_LOGIN_OK = 1;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		mContext = LoginActivity.this;
		requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
		setContentView(R.layout.login);
		setSupportProgressBarIndeterminateVisibility(false);
		// reference all used view
		txtInfo = (TextView) findViewById(R.id.txt_info);
		txtUserName = (EditText) findViewById(R.id.edit_username);
		btnLogin = (Button) findViewById(R.id.btn_client_login);
		btnStartScan = (Button) findViewById(R.id.btn_startscan);
		btnRelogin = (Button) findViewById(R.id.btn_relogin);
		btnRelogin.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View v) {
				txtInfo.setText(R.string.login_hint);
				findViewById(R.id.cont_login).setVisibility(View.VISIBLE);
				findViewById(R.id.cont_loggedin).setVisibility(View.GONE);

			}
		});

		btnLogin.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View v) {
				setSupportProgressBarIndeterminateVisibility(true);
				new Thread(new Runnable() {

					@Override
					public void run() {
						getToken();
					}
				}).start();
			}
		});

		btnStartScan.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View v) {
				Intent intent = new Intent();
				intent.putExtra("token", token);
				intent.setClass(mContext, MainActivity.class);
				startActivity(intent);

			}
		});
	}

	private void getToken() {
		String userName = txtUserName.getText().toString().trim();
		if (!userName.equals("")) {
			try {
				token = BaseHttpClient.httpGet(LOGIN_URL + userName).trim();
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			System.out.println(">>>" + token);
		}
		if (token == null)
			return;
		if (token.equals("")) {
			handler.sendEmptyMessage(MSG_LOGIN_FAILED);
		} else {
			handler.sendEmptyMessage(MSG_LOGIN_OK);
		}
	}

	private Handler handler = new Handler() {

		@Override
		public void handleMessage(Message msg) {
			if (msg.what == MSG_LOGIN_OK) {
				// 成功獲得token
				setSupportProgressBarIndeterminateVisibility(false);

				txtInfo.setText(getString(R.string.token_info, token));
				findViewById(R.id.cont_login).setVisibility(View.GONE);
				findViewById(R.id.cont_loggedin).setVisibility(View.VISIBLE);
			} else if (msg.what == MSG_LOGIN_FAILED) {
				setSupportProgressBarIndeterminateVisibility(false);
				Toast.makeText(mContext, R.string.login_failed,
						Toast.LENGTH_SHORT).show();
			}
		}

	};
}

佈局檔案:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_vertical"
        android:orientation="horizontal" >

        <TextView
            android:id="@+id/txt_info"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="20dp"
            android:drawableLeft="@drawable/chat"
            android:drawablePadding="10dp"
            android:text="@string/login_hint" />
    </LinearLayout>

    <LinearLayout
        android:id="@+id/cont_loggedin"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:visibility="gone" >

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:paddingLeft="20dp"
            android:paddingRight="20dp"
            android:paddingTop="10dp" >

            <Button
                android:id="@+id/btn_startscan"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@string/start_scan" >
            </Button>

            <Button
                android:id="@+id/btn_relogin"
                android:layout_marginTop="10dp"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@string/relogin" >
            </Button>
        </LinearLayout>
    </LinearLayout>

    <LinearLayout
        android:id="@+id/cont_login"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical" >

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:paddingLeft="20dp"
            android:paddingRight="20dp"
            android:paddingTop="10dp" >

            <TextView
                android:layout_width="60dp"
                android:layout_height="wrap_content"
                android:text="@string/user_name" />

            <EditText
                android:id="@+id/edit_username"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:paddingLeft="20dp"
            android:paddingRight="20dp"
            android:paddingTop="10dp" >

            <Button
                android:id="@+id/btn_client_login"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@string/login" >
            </Button>
        </LinearLayout>
    </LinearLayout>

</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獲取,並往下傳遞

@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);
		getSupportActionBar().setDisplayHomeAsUpEnabled(true);
		autoFocusHandler = new Handler();
		preview = (FrameLayout) findViewById(R.id.cameraPreview);
		// mCamera = getCameraInstance();
		Intent intent = getIntent();
		token = intent.getStringExtra("token");
		if (token == null || token.equals(""))
			finish();
	}

在掃描完成的回撥裡,我們將掃描獲得sessionID和token一起往下一個activity傳遞
PreviewCallback previewCb = new PreviewCallback() {
		public void onPreviewFrame(byte[] data, Camera camera) {
			Camera.Parameters parameters = camera.getParameters();
			Size size = parameters.getPreviewSize();

			Image barcode = new Image(size.width, size.height, "Y800");
			barcode.setData(data);

			int result = scanner.scanImage(barcode);
			String qrcodeString = null;
			if (result != 0) {
				previewing = false;
				mCamera.setPreviewCallback(null);
				mCamera.stopPreview();

				SymbolSet syms = scanner.getResults();
				for (Symbol sym : syms) {
					qrcodeString = sym.getData();
				}

			}
			if (qrcodeString != null) {
				Intent intent = new Intent();
				intent.setClass(MainActivity.this, ConfirmActivity.class);
				intent.putExtra("qrcodestring", qrcodeString);
				intent.putExtra("token", token);
				startActivity(intent);
			}

		}
	};

在完成掃描的回撥裡,我們把qrcodestring和token都提交給下一個activity

接著,我們來寫第三個activity

仍然建立一個類整合sherlockactivity,類名ConfirmActivity

這個activity在啟動的時候,也就意味著,掃描成功了,那麼就先通知伺服器端,掃描成功,頁面也會即時展示出掃描成功,等待手機確認登入的資訊

接下來,如果點確認登入,則通知伺服器確認登入。

因此我們有2個介面

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

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

一個是通知伺服器已經成功掃描的http介面,一個是通知伺服器確認登入的介面。引數都是sessionID,也就是二維碼帶的資訊,和使用者token。
package com.zbiti.qrcodelogin.activity;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;

import com.actionbarsherlock.app.SherlockActivity;
import com.actionbarsherlock.view.MenuItem;
import com.zbiti.qrcodelogin.R;
import com.zbiti.qrcodelogin.util.BaseHttpClient;

public class ConfirmActivity extends SherlockActivity {
	private Context mContext;
	private String sessionID;
	private String token;
	private final static String SCANNED_URL = "http://192.168.111.109:8000/scanned?";
	private final static String CONFIRMLOGIN_URL = "http://192.168.111.109:8000/confirmed?";
	private final static int LOGIN_SUCCESS=1;
	private final static int LOGIN_FAIL=0;

	private Button btnConfirmLogin;
	private Button btnCancel;
	private TextView txtInfo;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		mContext = ConfirmActivity.this;
		setContentView(R.layout.confirm_login);
		getSupportActionBar().setDisplayHomeAsUpEnabled(true);
		btnConfirmLogin = (Button) findViewById(R.id.btn_login);
		btnCancel = (Button) findViewById(R.id.btn_cancel);
		txtInfo=(TextView)findViewById(R.id.txt_confirm_info);

		btnConfirmLogin.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View v) {
				new Thread(new Runnable() {

					@Override
					public void run() {
						notifyConfirmed();

					}
				}).start();

			}
		});
		btnCancel.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View v) {
				finish();
			}
		});

		Intent intent = getIntent();
		sessionID = intent.getStringExtra("qrcodestring");
		token = intent.getStringExtra("token");
		if (sessionID == null || sessionID == null) {
			finish();
		}
		new Thread(new Runnable() {

			@Override
			public void run() {
				notifyScanned();
			}
		}).start();

	}

	private void notifyConfirmed() {
		String url = CONFIRMLOGIN_URL + "token=" + token + "&sessionid="
				+ sessionID;
		String s = null;
		try {
			s = BaseHttpClient.httpGet(url);
			if(s.equals("confirmed")){
				handler.sendEmptyMessage(LOGIN_SUCCESS);
			}else{
				handler.sendEmptyMessage(LOGIN_FAIL);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}

	}

	private void notifyScanned() {
		String url = SCANNED_URL + "token=" + token + "&sessionid=" + sessionID;
		String s = null;
		try {
			s = BaseHttpClient.httpGet(url);
		} catch (Exception e) {
			e.printStackTrace();
		}

	}
	private Handler handler= new Handler(){

		@Override
		public void handleMessage(Message msg) {
			if(msg.what==LOGIN_FAIL){
				txtInfo.setText(R.string.pc_login_fail);
			}else if(msg.what==LOGIN_SUCCESS){
				btnConfirmLogin.setVisibility(View.GONE);
				btnCancel.setVisibility(View.GONE);
				txtInfo.setText(R.string.pc_login_succuess);
			}
		}
		
	};

	@Override
	public boolean onMenuItemSelected(int featureId, MenuItem item) {
		if (item.getItemId() == android.R.id.home) {
			finish();
		}
		return super.onMenuItemSelected(featureId, item);
	}
}

佈局如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginBottom="10dp"
        android:layout_marginLeft="40dp"
        android:layout_marginRight="40dp"
        android:layout_marginTop="10dp"
        android:src="@drawable/pcs" />

    <TextView android:id="@+id/txt_confirm_info"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="@string/confirm_login_label"
        android:textSize="18sp" />

    <Button android:id="@+id/btn_login"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:layout_marginLeft="40dp"
        android:layout_marginRight="40dp"
        android:text="@string/btn_confirm_login" />

    <Button android:id="@+id/btn_cancel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="40dp"
        android:layout_marginRight="40dp"
        android:text="@string/btn_cancel" />

</LinearLayout>

這樣一個手機客戶端就完成了,其中用到的http請求的過程如下:
public static String httpGet(String url) throws Exception{
		String result = null;
		HttpClient client = new DefaultHttpClient(); 
		HttpGet get = new HttpGet(url);
		HttpResponse response = client.execute(get);
		if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { 
			InputStream is = response.getEntity().getContent();
			result=inStream2String(is);
		}
		return result;
	}
	
	public static String inStream2String(InputStream is) throws Exception {  
        ByteArrayOutputStream baos = new ByteArrayOutputStream();  
        byte[] buf = new byte[1024];  
        int len = -1;  
        while ((len = is.read(buf)) != -1) {  
            baos.write(buf, 0, len);  
        }  
        return new String(baos.toByteArray());  
    } 

string如下

<resources>
    <string name="app_name">二維碼登入客戶端</string>
    <string name="confirm_login_title">已經掃描,請確認登入</string>
    <string name="confirm_login_label">即將在瀏覽器上登入系統\n請確認是否是本人操作</string>
    <string name="btn_confirm_login">我確認登入系統</string>
    <string name="btn_cancel">取消</string>
    <string name="user_name">使用者名稱</string>
    <string name="user_password">密 碼</string>
    <string name="login">登入</string>
    <string name="relogin">重新登入</string>
    <string name="start_scan">開始掃描</string>
    <string name="login_failed">登入失敗</string>
    <string name="login_hint">隨便輸入使用者名稱,登入之後,伺服器會返回一個代表你身份的token。</string>
    <string name="token_info">您已經成功登入,token:\n %1$s</string>
    <string name="pc_login_succuess">網頁登入成功</string>
    <string name="pc_login_fail">網頁登入失敗,可能您掃描的頁面已過期!</string>
</resources>

原創文章,轉載請註明出處

測試程式在執行時可以設定伺服器地址,伺服器我們在下一篇會介紹。