Yêu cầu
- Biết chút ít về lập trình ứng dụng trên android
- Đã đọc qua phần 2 :|
Xây dựng project
Các bạn mở android studio và tạo mới 1 project tên là MyApp với minsdk là 16 bước tiếp theo chọn EmptyActivity.Sau khi tạo project bạn chọn như trong hình:
Tiếp theo kéo thả 2 files là mpandroidchartlibrary-2-1-6.jar và shared.jar vào folder libs như hình. Chú ý, file shared.jar là từ shared project(ở phần 2) export ra, để export ra thư viện các bạn làm như sau: click chuột phải vào shared project và chọn export, cửa sổ hiện ra chọn tiếp jar file rồi sau đó next và chọn nơi lưu file jar sau đó next cho đến hết :|
Sau khi thêm 2 file jar vào project các bạn chọn 2 file thư viện đó rồi click chuột phải chọn add as library… và OK. Như thế 2 thư viện trên đã được link vào project android của chúng ta.
Bước chuẩn bị project đã hoàn tất, bây giờ đến phần chính!
Xây dựng phần kết nối
Phần này là phần trọng tâm của chúng ta, nhiệm vụ của nó sẽ là kết nối đến server sau đó gửi yêu cầu và nhận lại phản hồi từ server. Mỗi lần gửi yêu cầu đến server thì sẽ tự động kèm theo thông tin đăng nhập của người dùng.Tạo mới 1 class tên là ConnectionServer trong package lib với nội dung như sau, phần giải thích nằm ngay trong code :|
public final class ConnectionServer {
private static ConnectionServer instance;
// kích thước bộ đệm đọc dữ liệu
private static final int BUFFER_SIZE = 2048;
// địa chỉ ip của server
private String mIPv4;
// port của server
private int mPort;
// tên web service, ở đây sẽ là MyWS
private final String mServerName;
// tài khoản để gửi cho server xác thực mỗi lần gửi yêu cầu
private Account mAccount;
// kiểm tra xem thông tin server đã đầy đủ chưa
public boolean ready() {
return (mIPv4 != null && mIPv4.length() > 0) && (mPort > -1);
}
// khởi tạo class với các thông số mặt định, thực ra ko cần cũng đc :|
private ConnectionServer() {
mPort = 8080;
mServerName = "MyWS";
mIPv4 = "192.168.1.111";
}
public static ConnectionServer getInstance() {
if (instance == null)
instance = new ConnectionServer();
return instance;
}
// thay đổi tài khoản người dùng, mỗi lần login thì nên gán lại :|
public void setAccount(Account account) {
mAccount = account;
}
public void setIPv4(String addr) {
this.mIPv4 = addr;
}
public void setPort(int port) {
mPort = port;
}
// lấy chuổi http từ tên của servlet và đối số
private String getHttpString(String servletName, String params) {
String http = String.format("http://%s:%d/%s/%s", mIPv4, mPort, mServerName, servletName);
if (params != null && params.length() > 0)
return String.format("%s?%s", http, params);
return http;
}
// hàm chính! nó sẽ gửi yêu cầu đến server thông qua chuổi httpUrl và sau đó nhận lại phản hồi
private TransmissionObject sendForResult(String httpUrl) {
TransmissionObject content = null;
HttpURLConnection cnn = null;
try {
URL url = new URL(httpUrl);
cnn = (HttpURLConnection) url.openConnection();
byte[] buff = new byte[BUFFER_SIZE];
int ret = cnn.getInputStream().read(buff);
byte[] data = new byte[ret];
System.arraycopy(buff, 0, data, 0, data.length);
content = CrossFlatform.fromBytes(data);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (cnn != null)
cnn.disconnect();
}
return content;
}
private TransmissionObject sendForResult(String servletName, String params) {
String httpUrl = getHttpString(servletName, params);
return sendForResult(httpUrl);
}
// kiểm tra xem tài khoản người dùng có chính xác không
@Nullable
public Account checkLogin(Account account) {
String params = String.format("req=check&username=%s&passhash=%s",
account.getUserName(), account.getPasswordHash());
TransmissionObject obj = sendForResult("AccountServlet", params);
if (obj == null)
return null;
int code = obj.getCode();
if (code == TransmissionObject.CODE_AUTH_ERR || code == TransmissionObject.CODE_DATA_NULL)
return null;
return (Account) obj.getData();
}
// gửi dữ yêu cầu cho server có tự động kèm theo thông tin đăng nhập và sau đó trả về dữ liệu phản hồi.
private TransmissionObject sendWithAuthentication(String servletName, String params) {
params = String.format("username=%s&passhash=%s&userid=%d&%s",
mAccount.getUserName(), mAccount.getPasswordHash(), mAccount.getId(), params);
return sendForResult(servletName, params);
}
// lấy toàn bộ thiết bị từ csdl trên server
@Nullable
@SuppressWarnings("unchecked")
public List<Device> getDevices() {
String params = "req=all";
TransmissionObject obj = sendWithAuthentication("DeviceServlet", params);
if (obj == null)
return null;
if (obj.getCode() != TransmissionObject.CODE_DATA_OK)
return null;
return (List<Device>) obj.getData();
}
// lấy thông tin real-time của 1 thiết bị bởi id của nó
@Nullable
@SuppressWarnings("unchecked")
public RealTime getRealtime(int deviceId) {
String params = String.format("id=%d", deviceId);
TransmissionObject obj = sendWithAuthentication("RealTimeServlet", params);
if (obj == null)
return null;
if (obj.getCode() != TransmissionObject.CODE_DATA_OK)
return null;
return (RealTime) obj.getData();
}
// lấy thông tin điện năng của 1 thiết bị trong 1 ngày
@Nullable
@SuppressWarnings("unchecked")
public float[] getEnergies(int deviceId, int day, int month, int year) {
String params = String.format("req=day&id=%d&day=%d&month=%d&year=%d", deviceId, day, month, year);
TransmissionObject obj = sendWithAuthentication("EnergyServlet", params);
if (obj == null)
return null;
if (obj.getCode() != TransmissionObject.CODE_DATA_OK)
return null;
return (float[]) obj.getData();
}
// lấy thông tin điện năng của 1 thiết bị trong 1 tháng
@Nullable
@SuppressWarnings("unchecked")
public float[] getEnergies(int deviceId, int month, int year) {
String params = String.format("req=month&id=%d&month=%d&year=%d", deviceId, month, year);
TransmissionObject obj = sendWithAuthentication("EnergyServlet", params);
if (obj == null)
return null;
if (obj.getCode() != TransmissionObject.CODE_DATA_OK)
return null;
return (float[]) obj.getData();
}
// lấy thông tin điện năng của 1 thiết bị trong 1 năm
@Nullable
@SuppressWarnings("unchecked")
public float[] getEnergies(int deviceId, int year) {
String params = String.format("req=year&id=%d&year=%d", deviceId, year);
TransmissionObject obj = sendWithAuthentication("EnergyServlet", params);
if (obj == null)
return null;
if (obj.getCode() != TransmissionObject.CODE_DATA_OK)
return null;
return (float[]) obj.getData();
}
// đổi tên thiết bị
public boolean renameDevice(int deviceId, String newName) {
String params = String.format("req=rename&id=%d&newname=%s", deviceId, newName);
TransmissionObject obj = sendWithAuthentication("DeviceServlet", params);
if (obj == null)
return false;
if (obj.getCode() != TransmissionObject.CODE_DATA_OK)
return false;
return obj.getData().toString().equals("OK");
}
// bật/tắt thiết bị
public boolean turnDevice(int deviceId, boolean off) {
String params = String.format("req=turn&id=%d&off=%d", deviceId, off ? 1 : 0);
TransmissionObject obj = sendWithAuthentication("DeviceServlet", params);
if (obj == null)
return false;
if (obj.getCode() != TransmissionObject.CODE_DATA_OK)
return false;
return obj.getData().toString().equals("OK");
}
// thay đổi mật khẩu đăng nhập của người dùng hiện tại
public boolean changePassword(String newPasswordHash) {
String params = String.format("req=changepass&npasshash=%s", newPasswordHash);
TransmissionObject obj = sendWithAuthentication("AccountServlet", params);
if (obj == null)
return false;
if (obj.getCode() != TransmissionObject.CODE_DATA_OK)
return false;
return obj.getData().toString().equals("OK");
}
}
Lớp HttpURLConnection giúp ta gửi chuổi request đến 1 web service bất kỳ và lấy về dữ liệu phản hồi của nó.
@SuppressWarnings(“unchecked”), xóa nó đi cũng không sao đâu :| chỉ tại nó warning nhìn nhứt mắt quá nên phải thêm nó vào thôi :|
Xây dựng trang home
Chức năng chính của trang home là hiển thị các thông tin cơ bản như server, account…và cho phép xem, điều khiển các thiết bị. Trang home sẽ xây dựng dựa trên trang main đã tạo ra mặt định lúc tạo project. Giao diện khi thiết kế sẽ như sau:Các chức năng lần lược như sau:
1. Phần username hiển thị tên người dùng hiện tại với 2 chức năng là thay đổi tài khoản và thay đổi mật khẩu.
2. Server IP và Port hiển thị địa chỉ IP và port của server mà ứng dụng sẻ kết nối đến, người dùng có thể thay đổi các thông tin này bằng cách chọn vào nút hình cờ lê bên phải.
3. Devices cho phép mở ra cửa sổ mới để hiển thị các thiết bị hiện có trong csdl của server và từ đó người dùng có thể xem lịch sử sử dụng điện năng cũng như giám sát các thiết bị của mình.
4. Phần cuối cùng là superuser, cho phép người dùng superuser có thể quản lý các tài khoản hiện có trong csdl của server bằng giao diện web.
Về phần thiết kế giao diện home khá đơn giản vì thế mình sẽ ko show ra ở đây, các bạn có thể xem thêm trong project(link cuối bài viết).
Trước khi đi vào phần xử lý chúng ta xây dựng lớp InputBox để hiển thị hộp thoại nhập thông tin. Đầu tiên tạo mới 1 layout tên là dialog_inputbox, thiết kế giao diện như sau:
Giao diện InputBox gồm 5 phần chính là tiêu đề(mặt định là tên ứng dụng), thông điệp hiển thị, vùng nhập text và 2 nút bấm xác nhận.
Tạo lớp InputBox trong package ui, phần xử lý của InputBox như sau:
public class InputBox implements View.OnClickListener {
// hộp thoại sẽ hiển thị
private Dialog mDialog;
// tiêu đề của dialog
private TextView mTitleView;
// thông điệp của dialog
private TextView mMessageView;
// vùng nhập text
private EditText mInputView;
// đăng ký sự kiện khi người dùng nhập xong và nhấn OK
private OnInputCompletedListener mOnInputCompletedListener;
public void setOnInputCompletedListener(OnInputCompletedListener listener) {
mOnInputCompletedListener = listener;
}
public void setTitle(CharSequence title) {
mTitleView.setText(title);
}
public void setMessage(CharSequence message) {
mMessageView.setText(message);
}
public void show() {
mDialog.show();
}
public void dismiss() {
mDialog.dismiss();
}
public InputBox(Context context, String initValue) {
mDialog = new Dialog(context);
mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
mDialog.setContentView(R.layout.dialog_inputbox);
mDialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
mTitleView = (TextView) mDialog.findViewById(R.id.inputbox_tv_title);
mMessageView = (TextView) mDialog.findViewById(R.id.inputbox_tv_message);
mInputView = (EditText) mDialog.findViewById(R.id.inputbox_et_enter);
mDialog.findViewById(R.id.inputbox_btn_cancel).setOnClickListener(this);
mDialog.findViewById(R.id.inputbox_btn_ok).setOnClickListener(this);
mInputView.setText(initValue);
}
public InputBox(Context context) {
this(context, null);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.inputbox_btn_cancel) {
mDialog.dismiss();
} else if (v.getId() == R.id.inputbox_btn_ok) {
if (mOnInputCompletedListener != null)
mOnInputCompletedListener.inputCompleted(this, mInputView.getText().toString());
}
}
public interface OnInputCompletedListener {
void inputCompleted(InputBox box, @NonNull String content);
}
}
InputBox sẽ cho phép ta hiển thị hộp thoại nhập liệu và khi người dùng nhập xong và nhấn nút OK thì sự kiện OnInputCompletedListener sẽ được raise, nếu người dùng nhấn Cancel thì sự kiện này sẽ không được raise. Ta dựa vào đối số content của sự kiện để xác định nội dung mà người dùng đã nhập vào là gì.Tạo lớp Config chứa các key lưu trữ dữ liệu cục bộ của ứng dụng như lưu tên đăng nhập, mật khẩu, thông tin server…
public final class Config {
// tên của sharedPreferenced
public static final String SHARED_PREF_NAME = "shared_pref";
// key lưu username khi người dùng login
public static final String SHARED_PREF_ACC_USERNAME = "account_username";
// key lưu password hash
public static final String SHARED_PREF_ACC_PASSWORDHASH = "account_passwordhash";
// key lưu user id, id này chỉ có khi người dùng đăng nhập thành công vào hệ thống
public static final String SHARED_PREF_ACC_ID = "account_id";
// key lưu địa chỉ của server
public static final String SHARED_PREF_SRV_IP = "server_ip";
// key lưu port của server
public static final String SHARED_PREF_SRV_PORT = "server_port";
private Config() {
}
}
Quay lại lớp MainActivity, ở đây chúng ta sẽ xử lý phần logic của trang home. Các phương thức chính của lớp này như sau:// hiển thị danh sách các thiết bị hiện có, trang DevicesActivity sẽ được đề cập ở phần dưới
private void displayDevices() {
startActivity(new Intent(this, DevicesActivity.class));
}
// thay đổi tài khoản đăng nhập, nó sẽ xóa thông tin đăng nhập cũ khỏi csdl cục bộ sau đó gọi phương thức checkLogin để hiển thị cửa sổ đăng nhập
private void changeUserName() {
SharedPreferences sharedPref = getSharedPref();
SharedPreferences.Editor editor = sharedPref.edit();
editor.remove(Config.SHARED_PREF_ACC_ID);
editor.apply();
mLogged = false;
checkLogin();
}
// đi đến trang quản lý người dùng của superuser bởi việc mở trang web với link đến servelet superuser
private void gotoSuperUser() {
SharedPreferences sharedPref = getSharedPref();
String link = String.format(
"http://%s:%d/MyWS/SuperuserServlet",
sharedPref.getString(Config.SHARED_PREF_SRV_IP, ""),
sharedPref.getInt(Config.SHARED_PREF_SRV_PORT, 8080));
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(link));
startActivity(browserIntent);
}
// hiển thị hộp thoại thay đổi địa chỉ ip
private void changeIP() {
InputBox box = new InputBox(this, getSharedPref().getString(Config.SHARED_PREF_SRV_IP, ""));
box.setOnInputCompletedListener(new InputBox.OnInputCompletedListener() {
// lưu vào csdl cục bộ và cập nhật lại thông tin kết nối ra trang home đồng thời thay đổi trong ConnectionServer
@Override
public void inputCompleted(InputBox box, @NonNull String content) {
if (content.length() > 0) {
SharedPreferences sharedPref = getSharedPref();
SharedPreferences.Editor editor = sharedPref.edit();
editor.putString(Config.SHARED_PREF_SRV_IP, content);
editor.apply();
settingConnection();
box.dismiss();
} else {
Toast.makeText(MainActivity.this, "IP not empty!", Toast.LENGTH_SHORT).show();
}
}
});
box.setMessage("Enter new server IP:");
box.show();
}
// tương tự với phương thức trên, phương thức này hiển thị hộp thoại nhập port của server
private void changePort() {
InputBox box = new InputBox(this, String.format("%d", getSharedPref().getInt(Config.SHARED_PREF_SRV_PORT, 2512)));
box.setOnInputCompletedListener(new InputBox.OnInputCompletedListener() {
// port đc nhập sẽ được cập nhật ra giao diện và cả ConnectionServer, đồng thời thông tin sẽ được lưu vào csdl cục bộ
@Override
public void inputCompleted(InputBox box, @NonNull String content) {
if (content.length() > 0) {
int port = Convert.parseInt(content, -1);
if (port > -1) {
SharedPreferences sharedPref = getSharedPref();
SharedPreferences.Editor editor = sharedPref.edit();
editor.putInt(Config.SHARED_PREF_SRV_PORT, port);
editor.apply();
settingConnection();
box.dismiss();
} else {
Toast.makeText(MainActivity.this, "Port must is numbers!", Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(MainActivity.this, "Port not empty!", Toast.LENGTH_SHORT).show();
}
}
});
box.setMessage("Enter new server Port:");
box.show();
}
Đây chỉ mới là 1 phần của trang home, còn 1 số phương thức xử lý khác các bạn có thể xem thêm trong project. Như ở trên xuất hiện 2 trang nữa đó là trang login và trang xem danh sách thiết bị, ta sẽ lần lược xây dựng chúng ngay bên dưới.Xây dựng trang login
Nhiệm vụ của trang này là cho phép người dùng đăng nhập vào server, cụ thể như sau:Khi người dùng đăng nhập thành công thì thông tin đăng nhập tự động lưu vào csdl cục bộ và chuyển đến trang home.
Các bạn tạo mới 1 activity và đặt tên là LoginActivity. Phần giao diện ta thiết kế gồm 1 ô nhập username, 1 ô nhập password, 1 nút đăng nhập và 1 progressbar(sẽ hiển thị trong lúc check login):
Phần code xml các bạn có thể xem thêm trong project, ở đây có lớp đặt biệt là android.support.design.widget.TextInputLayout, nhiệm vụ của nó là thu nhỏ hint của TextView lại trong lúc nhập.
Phần xử lý logic trong lớp LoginActivity gồm 2 phần chính như sau:
// phương thức này sẽ kiểm tra thông tin nhập vào có hợp lệ không trước khi gửi tới server để kiểm tra
private void attemptLogin() {
if (mAuthTask != null) {
return;
}
// Reset errors.
mUsernameView.setError(null);
mPasswordView.setError(null);
// Store values at the time of the login attempt.
String username = mUsernameView.getText().toString();
String password = mPasswordView.getText().toString();
boolean cancel = false;
View focusView = null;
// Check for a valid password, if the user entered one.
if (!TextUtils.isEmpty(password) && !isPasswordValid(password)) {
mPasswordView.setError(getString(R.string.error_invalid_password));
focusView = mPasswordView;
cancel = true;
}
// Check for a valid username.
if (TextUtils.isEmpty(username)) {
mUsernameView.setError(getString(R.string.error_field_required));
focusView = mUsernameView;
cancel = true;
} else if (!isUsernameValid(username)) {
mUsernameView.setError(getString(R.string.error_invalid_email));
focusView = mUsernameView;
cancel = true;
}
if (cancel) {
// There was an error; don't attempt login and focus the first
// form field with an error.
focusView.requestFocus();
} else {
// Show a progress spinner, and kick off a background task to
// perform the user login attempt.
showProgress(true);
mAuthTask = new UserLoginTask(username, password);
mAuthTask.execute((Void) null);
}
}
// lớp này sẽ gửi dữ liệu đã nhập để kiểm tra xem có hợp lệ không
public class UserLoginTask extends AsyncTask<Void, Void, Account> {
private final String mUsername;
private final String mPassword;
UserLoginTask(String username, String password) {
mUsername = username;
mPassword = password;
}
// kiểm tra xem thông tin đăng nhập có hợp lệ không(ở trong 1 thread khác thread chính)
@Override
protected Account doInBackground(Void... params) {
Account account = new Account();
account.setUserName(mUsername);
account.setPasswordHash(Security.getPasswordHash(mPassword));
return ConnectionServer.getInstance().checkLogin(account);
}
// khi kiểm tra xong thì kiểm tra xem kết quả trả về như thế nào. nếu đăng nhập thành công thì đóng login lại. dữ liệu sẽ được lưu vào csdl cục bộ và hiển thị thông báo đăng nhập thành công/thất bại
@Override
protected void onPostExecute(final Account account) {
mAuthTask = null;
showProgress(false);
String username = null;
String passwordHash = null;
int id = -1;
if (account != null) {
Toast.makeText(LoginActivity.this, "Login success!", Toast.LENGTH_SHORT).show();
username = account.getUserName();
passwordHash = account.getPasswordHash();
id = account.getId();
setResult(RESULT_OK);
finish();
} else {
mPasswordView.setError(getString(R.string.error_incorrect_password));
mPasswordView.requestFocus();
}
SharedPreferences.Editor editor = mSharedPref.edit();
editor.putString(Config.SHARED_PREF_ACC_USERNAME, username)
.putString(Config.SHARED_PREF_ACC_PASSWORDHASH, passwordHash)
.putInt(Config.SHARED_PREF_ACC_ID, id);
editor.apply();
}
@Override
protected void onCancelled() {
mAuthTask = null;
showProgress(false);
}
}
Chú ý rằng việc kiểm tra tài khoản người dùng sẽ được thực thi trong background thread để tránh block giao diện và quan trọng hơn là tránh báo lỗi :| vì mọi thao tác truyền dữ liệu qua network đều phải được thực thi ở background thread trong android :|Như thế là trang login đã xong, tiếp theo sẽ là trang làm việc chính của ta, trang xem danh sách thiết bị.
Cũng vì 2 lý do trên mà mọi thao tác liên quan đến gửi yêu cầu và nhận phản hồi từ server đều phải được thực thi ở background thread.
Xây dựng trang xem danh sách thiết bị
Đầu tiên ta xây dựng cửa sổ loading để hiển thị lúc gửi và nhận yêu cầu từ phía server. Tạo layout mới tên là dialog_processingbox, thiết kế giao diện khá đơn giản chỉ gồm 1 LinearLayout chứa ProgressBar. Ở ProgressBar đặt thuộc tính indeterminate là true để nó quay mãi :|.<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="50dp">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/progressBar"
android:layout_gravity="center"
android:indeterminate="true" />
</LinearLayout>
Phần xử lý như sau:public class ProcessingBox {
private Dialog mDialog;
public ProcessingBox(Context context) {
mDialog = new Dialog(context);
// không hiển thị tiêu đều của dialog
mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
mDialog.setContentView(R.layout.dialog_processingbox);
// không cho phép người dùng đóng dialog này
mDialog.setCancelable(false);
}
public void show() {
mDialog.show();
}
public void hide() {
mDialog.hide();
}
public void dismiss() {
mDialog.dismiss();
}
}
Chỉ đơn giảnlà tạo dialog mới với giao diện là 1 processingbar thôi.Tạo mới 1 activity tên là DevicesActivity, phần giao diện chỉ gồm 1 ListView để hiển thị danh sách các thiết bị.
Để hiển thị custom item trong listview như trên hình ta tạo thêm 1 file layout mới tên là device_item với code như sau:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="5dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="Name"
android:id="@+id/device_item_tv_name" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:text="ID"
android:id="@+id/device_item_tv_id" />
</LinearLayout>
Chuyển đến lớp DevicesActivity, tạo mới 1 custom adapter bên trong class này để hiển thị thông tin thiết bị ra listview:// lớp lưu giữ thông tin thiết bị và tình trạng thiết bị, mỗi thực thể DeviceHolder sẽ tương ứng với 1 item trong listview
private class DeviceHolder {
public Device device = null;
public byte state = 0;
}
// adapter kế thừa từ BaseAdapter để quy định cách hiển thị dữ liệu ra listview
private class DeviceAdapter extends BaseAdapter {
// danh sách các thiết bị và tình trạng tương ứng của nó
private final List<DeviceHolder> devices;
// dùng LayoutInflater để tạo mới view từ xml layout
private final LayoutInflater inflater;
public DeviceAdapter(Context context, List<DeviceHolder> devices) {
inflater = (LayoutInflater) context.getSystemService(LAYOUT_INFLATER_SERVICE);
this.devices = devices;
}
@Override
public int getCount() {
return devices.size();
}
@Override
public Object getItem(int position) {
return devices.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public int getViewTypeCount() {
return 1;
}
@Override
public int getItemViewType(int position) {
return 1;
}
// khi thiết bị đang hoạt động hoặc đang tắt sẽ có màu sắc hiển thị ở listview item khác nhau
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = inflater.inflate(R.layout.device_item, null);
holder = new ViewHolder();
holder.nameView = (TextView) convertView.findViewById(R.id.device_item_tv_name);
holder.idView = (TextView) convertView.findViewById(R.id.device_item_tv_id);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
DeviceHolder deviceHolder = devices.get(position);
holder.nameView.setText(deviceHolder.device.getName());
holder.idView.setText(String.format("ID: %d", deviceHolder.device.getId()));
int colorId = deviceHolder.state != 0 ? R.color.colorActive : R.color.colorInactive;
int color = ContextCompat.getColor(DevicesActivity.this, colorId);
holder.nameView.setTextColor(color);
return convertView;
}
}
// lưu giữ các view của listview item tránh trường hợp tìm kiếm lại view mất thời gian
private static class ViewHolder {
TextView nameView;
TextView idView;
}
Khi activity này được khởi tạo thì nó sẽ tự động lấy danh sách các thiết bị từ server sau đó hiển thị lên listview, sau khi lấy dữ liệu thành công thì nó sẽ bắt đầu quá trình cập nhật trạng thái hoạt động của các thiết bị 1 cách liên tục. Code thực thi chính của phần này như sau:// hiển thị hộp thoại loading trong khi gửi yêu cầu và nhận phản hồi từ server
processingBox = new ProcessingBox(this);
processingBox.show();
// công việc sẽ diễn ra ở background thread
new Thread(new Runnable() {
@Override
public void run() {
final List<Device> devices = ConnectionServer.getInstance().getDevices();
// các công việc liên quan đến hiển thị ra giao diện đều phải thực thi từ thread chính
runOnUiThread(new Runnable() {
@Override
public void run() {
// tắt hộp thoại loading sau khi nhận xong phản hồi từ server
processingBox.dismiss();
if (devices == null) {
Toast.makeText(DevicesActivity.this, "Network error!", Toast.LENGTH_LONG).show();
finish();
} else {
// khởi tạo danh sách các DeviceHolder cho mỗi thiết bị để hiển thị ra listview
List<DeviceHolder> holders = new ArrayList<DeviceHolder>(devices.size());
for (Device device : devices) {
DeviceHolder holder = new DeviceHolder();
holder.device = device;
holder.state = 0;
holders.add(holder);
}
adapter = new DeviceAdapter(DevicesActivity.this, holders);
// hiển thị ra listview
devicesView.setAdapter(adapter);
// bắt đầu cập nhật trạng thái thiết bị
updateDevicesState(holders);
}
}
});
}
}).start();
Phần cập nhật tình trạng thiết bị, ta chỉ cần lấy thông tin real-time của mỗi thiết bị sau 1 khoản thời gian và sau đó cập nhật lại trong danh sách DeviceHolder, quá trình sẽ lặp đi lặp lại sau 1 khoản thời gian nhất định.private void updateDevicesState(final List<DeviceHolder> devices) {
pendingStop = false;
// công việc sẽ được thực hiện ở 1 background thread
new Thread(new Runnable() {
@Override
public void run() {
boolean brokenDown = false;
// lặp đi lặp lại quá trình kiểm tra trạng thái hoạt động của thiết bị
while (!pendingStop && !brokenDown) {
// cờ đảm bảo chỉ khi có dữ liệu thay đổi thì mới cần cập nhật ra giao diện
boolean changed = false;
// kiểm tra tình trạng của mỗi thiết bị trong danh sách
for (DeviceHolder holder : devices) {
RealTime realTime = ConnectionServer.getInstance().getRealtime(holder.device.getId());
if (realTime != null) {
if (holder.state != realTime.getState()) {
holder.state = realTime.getState();
changed = true;
}
} else {
brokenDown = true;
break;
}
}
// cập nhật lại giao diện nếu có thay đổi, quá trình cập nhật phải xảy ra ở thread chính vì thế cần gửi thông điệp yêu cầu cập nhật cho thread chính :|
if (changed)
updaterHandler.sendEmptyMessage(1);
long now = System.currentTimeMillis();
while (System.currentTimeMillis() - now < UPDATE_AFTER && !pendingStop && !brokenDown) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
pendingStop = true;
break;
}
}
}
}
}).start();
}
Ở đây mình sử dụng Handler để thay thế cho phương thức runOnUiThread để thực hiện công việc trong thread chính. Thực ra ở trong trường hợp này bạn sử dụng runOnUiThread đều được, mình chỉ sử dụng Hanlder để đổi gió thôi :|Tiếp theo là các tác vụ chính trên mỗi thiết bị hiển thị ở listview như sau:
Chú ý là các thao tác trên giao diện phải được thực thi ở thread chính nhé!
Ta sẽ sử dụng 1 context menu để hiển thị 4 tác vụ này khi người dùng nhấn giữ vào mỗi item ở listview, để làm được điều này bạn tạo mới 1 menu layout với tên devices_context_menu với nội dung như sau:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/devices_context_menu_realtime"
android:title="Real-time"/>
<item android:id="@+id/devices_context_menu_history"
android:title="History"/>
<item android:id="@+id/devices_context_menu_rename"
android:title="Rename"/>
<item android:id="@+id/devices_context_menu_turn"
android:title="Turn OFF"/>
</menu>
Code giao diện khá đơn giản vì thế mình không giải thích nhiều. Để hiển thị được context menu khi người dùng nhấn giữ vào từng listview item các bạn làm theo 2 bước sau:- Thêm phương thức registerForContextMenu(devicesView) khi activity được khởi tạo. Đối số của nó chính là listview của chúng ta.
- Ghi đè phương thức onCreateContextMenu của activity và khởi tạo menu từ xml layout bằng câu lệnh: getMenuInflater().inflate(R.menu.devices_context_menu, menu)
// phướng thức này sẽ được thực thi khi người dùng chọn 1 menu item từ context menu
@Override
public boolean onContextItemSelected(MenuItem item) {
// ta lấy thông tin về vị trị item được chọn từ đó suy ra được thiết bị được người dùng chọn
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
int position = info.position;
Device device = ((DeviceHolder) adapter.getItem(position)).device;
// người dùng lựa chọn xem real-time, mở activity xem real-time lên thôi :|
if (item.getItemId() == R.id.devices_context_menu_realtime) {
startTaskActivity(RealtimeActivity.class, device.getId());
// chọn xem lịch sử sử dụng điện năng, mở activity xem history lên
} else if (item.getItemId() == R.id.devices_context_menu_history) {
startTaskActivity(HistoryActivity.class, device.getId());
// đổi tên thiết bị
} else if (item.getItemId() == R.id.devices_context_menu_rename) {
renameDevice(device);
// bật tắt thiết bị
} else if (item.getItemId() == R.id.devices_context_menu_turn) {
turnOFF(device.getId(), item.getTitle().equals("Turn OFF"));
}
return true;
}
// đổi tên thiết bị, hiển thị inputbox cho người dùng nhập tên mới cho thiết bị.
private void renameDevice(final Device device) {
InputBox box = new InputBox(this, device.getName());
box.setMessage(String.format("Change name of %d", device.getId()));
box.setOnInputCompletedListener(new InputBox.OnInputCompletedListener() {
@Override
public void inputCompleted(final InputBox box, @NonNull final String content) {
new Thread(new Runnable() {
@Override
public void run() {
// gửi yêu cầu đổi tên tới server
final boolean ok = ConnectionServer.getInstance().renameDevice(device.getId(), content);
// cập nhật lại listview khi tên thiết bị thay đổi
runOnUiThread(new Runnable() {
@Override
public void run() {
if (ok) {
device.setName(content);
adapter.notifyDataSetChanged();
Toast.makeText(DevicesActivity.this, "OK", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(DevicesActivity.this, "Error!", Toast.LENGTH_LONG).show();
}
}
});
}
}).start();
box.dismiss();
}
});
box.show();
}
// bật tắt thiết bị, làm việc y chan công tắt :|
private void turnOFF(final int deviceId, final boolean off) {
new Thread(new Runnable() {
@Override
public void run() {
final boolean ok = ConnectionServer.getInstance().turnDevice(deviceId, off);
runOnUiThread(new Runnable() {
@Override
public void run() {
if (ok)
Toast.makeText(DevicesActivity.this, "OK", Toast.LENGTH_SHORT).show();
else
Toast.makeText(DevicesActivity.this, "Error!", Toast.LENGTH_LONG).show();
}
});
}
}).start();
}
// khởi động activity để xem lịch sử hoặc xem real-time, chỉ cần truyền id của thiết bị và bên kia sẽ dựa vào đó để lấy thông tin.
private void startTaskActivity(Class target, int deviceId) {
Intent intent = new Intent(this, target);
Bundle bundle = new Bundle();
bundle.putInt(TaskActivity.INTENT_DEVICE_ID, deviceId);
intent.putExtras(bundle);
startActivity(intent);
}
Như thế là phần xử lý chính của DevicesActivity đã xong, bây giờ ta còn 2 activity chính cần hoàn tất nữa là activity xem lịch sử điện năng và real-time.Xây dựng trang xem lịch sử điện năng
Đầu tiên tạo 1 lớp TaskActivity để 2 activity là xem real-time và xem lịch sử điện năng kế thừa với nội dung:public abstract class TaskActivity extends AppCompatActivity {
public static final String INTENT_DEVICE_ID = "device_id";
// lấy id của thiết bị cần xử lý
protected int getDeviceId() {
Intent intent = getIntent();
return intent.getExtras().getInt(INTENT_DEVICE_ID);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// tự động đặt tiêu đề cho activity
setTitle(String.format("Id: %d", getDeviceId()));
}
}
Xong! giờ tạo 1 activity mới tên là HistoryActivity, phần giao diện thiết kế như sau:Ở đây có mình sử dụng lớp com.github.mikephil.charting.charts.BarChart để hiển thị biểu đồ dạng cột của dữ liệu.
Về phần xử lý, full code như sau:
public class HistoryActivity extends TaskActivity implements View.OnClickListener, OnChartValueSelectedListener {
// id của thiết bị cần xem lịch sử
private int mDeviceId;
// view hiển thị biểu đồ
private BarChart mBarChart;
// nhập năm ở đây
private EditText mYearView;
// nhập tháng
private EditText mMonthView;
// và nhập ngày
private EditText mDayView;
// định dạng dữ liệu hiển thị, nôm na nó giống như %0.2f trong C :|
private DecimalFormat mFormat = new DecimalFormat("#.##");
.........................
// lấy thông tin điện năng của năm
private void loadYear() {
final ProcessingBox box = new ProcessingBox(this);
box.show();
new Thread(new Runnable() {
@Override
public void run() {
final float[] energies = ConnectionServer.getInstance().getEnergies(mDeviceId, getYear());
runOnUiThread(new Runnable() {
@Override
public void run() {
if (energies == null) {
Toast.makeText(HistoryActivity.this, "Error!", Toast.LENGTH_LONG).show();
} else {
mBarChart.getXAxis().resetLabelsToSkip();
// đảm bảo dữ liệu đủ 12 tháng
float[] data = new float[12];
for (int i = 0; i < energies.length; i++) {
data[i] = energies[i] / 1000.0f;
}
loadData(data);
}
box.dismiss();
}
});
}
}).start();
}
// lấy thông tin điện năng của tháng
private void loadMonth() {
final ProcessingBox box = new ProcessingBox(this);
box.show();
new Thread(new Runnable() {
@Override
public void run() {
final float[] energies = ConnectionServer.getInstance().getEnergies(
mDeviceId, getMonth(), getYear());
runOnUiThread(new Runnable() {
@Override
public void run() {
if (energies == null) {
Toast.makeText(HistoryActivity.this, "Error!", Toast.LENGTH_LONG).show();
} else {
mBarChart.getXAxis().setLabelsToSkip(1);
float[] data = new float[DateTime.getMaxDay(getMonth(), getYear())];
for (int i = 0; i < energies.length; i++) {
data[i] = energies[i] / 1000.0f;
}
loadData(data);
}
box.dismiss();
}
});
}
}).start();
}
// lấy thông tin điện năng của ngày
private void loadDay() {
final ProcessingBox box = new ProcessingBox(this);
box.show();
new Thread(new Runnable() {
@Override
public void run() {
final float[] energies = ConnectionServer.getInstance().getEnergies(
mDeviceId, getDay(), getMonth(), getYear());
runOnUiThread(new Runnable() {
@Override
public void run() {
if (energies == null) {
Toast.makeText(HistoryActivity.this, "Error!", Toast.LENGTH_LONG).show();
} else {
mBarChart.getXAxis().setLabelsToSkip(1);
float[] data = new float[24];
for (int i = 0; i < energies.length; i++) {
data[i] = energies[i] / 1000.0f;
}
loadData(data);
}
box.dismiss();
}
});
}
}).start();
}
// cài đặt biểu đồ như cài đặt màu, mô tả, blabla
protected void setupChart(BarChart chart) {
chart.setDescription("Energy(kWh)");
chart.setDrawBarShadow(false);
chart.setDrawGridBackground(false);
XAxis xAxis = chart.getXAxis();
xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
xAxis.setSpaceBetweenLabels(0);
xAxis.setDrawGridLines(false);
chart.getAxisRight().setDrawLabels(false);
chart.getLegend().setEnabled(false);
chart.animateY(1500);
chart.setOnChartValueSelectedListener(this);
}
// load dữ liệu đã lấy từ server lên biểu đồ
private void loadData(float[] data) {
// trục y, trục giá trị điện năng
ArrayList<BarEntry> yVals = new ArrayList<>(data.length);
// trục x, trục thời gian
ArrayList<String> labels = new ArrayList<>(data.length);
for (int i = 0; i < data.length; i++) {
yVals.add(new BarEntry(data[i], i));
labels.add(String.format("%d", i + 1));
}
BarDataSet dataSet = new BarDataSet(yVals, "");
dataSet.setDrawValues(false);
BarData barData = new BarData(labels, dataSet);
mBarChart.setData(barData);
mBarChart.invalidate();
}
// thực hiện lấy dữ liệu từ server khi người dùng nhân nút
@Override
public void onClick(View v) {
if (v.getId() == R.id.history_btn_day) {
loadDay();
} else if (v.getId() == R.id.history_btn_month) {
loadMonth();
} else if (v.getId() == R.id.history_btn_year) {
loadYear();
}
}
// hiển thị thông báo giá trị điện năng tương ứng khi người dùng chọn 1 cột bất kỳ
@Override
public void onValueSelected(Entry entry, int i, Highlight highlight) {
Toast.makeText(HistoryActivity.this, String.format("%s kWh", mFormat.format(entry.getVal())), Toast.LENGTH_SHORT).show();
}
}
Chúng ta thiết kế trang xem lịch sử sử dụng điện năng dạng truy vấn theo thời gian, người dùng sẽ nhập vào ngày tháng năm và chọn vào các nút tương ứng để xem điện năng của ngày, tháng hoặc năm. Khi nhấn nút thì giá trị thời gian sẽ được gửi cho server để nhận lại phản hồi về các giá trị điện năng tương ứng, khi ứng dụng có được các giá trị điện năng này thì nó sẽ hiển thị ra giao diện dặng biểu đồ cột gồm trục y là giá trị điện năng(kWh) và trục x là giá trị thời gian tương ứng. Khi người dùng touch vào 1 cột bất kỳ trên biểu đồ thì giá trị điện năng của cột đó sẽ hiển thị ra màng hình cho người dùng theo dỏi.Xây dưng trang xem real-time
Trang này sẽ hiển thị các giá trị thời gian thực như power, voltage, amperage của thiết bị. Ở đây mình sử dụng biểu đồ đường để vẽ ra real-time của thiết bị, cả 2 biểu đồ đường và biểu đồ cột đều có trong thư viện mpandoirdchart.Các bạn tạo mới 1 activity tên là RealtimeActivity. Về phần thiết kế giao diện khá đơn giản chỉ gồm 3 biểu đồ đường tương ứng với 3 giá trị real-time là power, voltage và amperage.
Code xml như sau:
.....
<com.github.mikephil.charting.charts.LineChart
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="@android:color/darker_gray"
android:id="@+id/realtime_lc_power"
android:layout_marginBottom="5dp"/>
<com.github.mikephil.charting.charts.LineChart
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="@android:color/darker_gray"
android:id="@+id/realtime_lc_amperage"
android:layout_marginBottom="5dp"/>
<com.github.mikephil.charting.charts.LineChart
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="@android:color/darker_gray"
android:id="@+id/realtime_lc_voltage"
android:layout_marginBottom="5dp"/>
</LinearLayout>
Để canh chiều cao 3 biểu đồ bằng nhau thì ta đặt layout_height là 0 cho mỗi biểu đồ sau đó đặt giá trị layout_weight bằng nhau cho cả 3 biểu đồ(ở trong code mình đặt cả 3 đều là 1).Về phần xử lý, đơn giản là ta sẽ lấy giá trị real-time của thiết bị sau đó hiển thị lên 3 biểu đồ rồi lặp lại quá trình sau 1 khoản thời gian nhất định. Nếu quá việc lấy dữ liệu real-time không thành công thì ứng dụng sẽ cố gắng “thử” lấy thêm vài lần nữa, nếu quá số lần quy định thì ứng dụng mới chính thức báo lỗi kết nối.
Phương thức cập nhật real-time chính, nó sẽ được thực thi ở background thread:
@Override
public void run() {
int lastError = 0;// count of try :|
int deviceId = mDeviceId;// id của thiết bị
// lặp lại quá trình cho đến khi có yêu cầu dừng
while (!pendingStop) {
RealTime realTime = ConnectionServer.getInstance().getRealtime(deviceId);
// lấy được real-time thì thông báo cho thread chính cập nhật lên giao diện
if (realTime != null) {
Message msg = mUpdaterHandler.obtainMessage(WHAT_OK, realTime);// everything OK
mUpdaterHandler.sendMessage(msg);
lastError = 0;
// không lấy được real-time thì kiểm tra xem số lần "thử" đã vược quá số lần quy định(ở đây là 5) chưa, nếu chưa thì chỉ yêu cầu hiện warning thôi
} else if (lastError < 5) {
mUpdaterHandler.sendEmptyMessage(WHAT_WARNING);// something wrong! try again...
lastError++;
} else {
mUpdaterHandler.sendEmptyMessage(WHAT_ERROR);// connection broken down so we display message and exit thread
break;
}
// ngừng 1 khoản thời gian trước khi tiếp tục cập nhật real-time
long start = System.currentTimeMillis();
while ((System.currentTimeMillis() - start < UPDATE_AFTER) && (!pendingStop)) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
}
}
Như đã thấy, phương thức trên sẽ gửi thông báo đến thread chính để yêu cầu nó cập nhật giao diện. Để làm được điều đó mình sử dụng Handler và các cờ WHAT_OK, WHAT_WARNING hay WHAT_ERROR để phân biệt giữa các loại yêu cầu hiển thị. Đối với WHAT_OK thì có kèm theo dữ liệu real-time để hiển thị nữa.Phương thức handleMessage sẽ nhận các thông điệp yêu cầu từ Handler, chúng ta sẽ phân biệt các yêu cầu này dựa vào trường what của đối số msg. Chú ý rằng phương thức handleMessage sẽ được thực thi ở thread chính.
@Override
public boolean handleMessage(Message msg) {
if (msg.what == WHAT_OK) {
updateRealtime((RealTime) msg.obj);
} else if (msg.what == WHAT_WARNING) {
displayWarning(mPowerChart);
displayWarning(mAmperageChart);
displayWarning(mVoltageChart);
} else if (msg.what == WHAT_ERROR) {
displayError(mPowerChart);
displayError(mAmperageChart);
displayError(mVoltageChart);
}
return true;
}
Hai phương thức là displayWarning và displayError chỉ đơn giản là hiển thị warning và error lên các biểu đồ tương ứng, các bạn có thể đọc thêm ở trong project. Phương thức updateRealtime sẽ phân tích các giá trị power, voltage và amperage từ thực thể RealTime nhận được sau đó hiển thị lên các biểu đồ.
private void updateRealtime(RealTime realTime) {
if (realTime == null)
return;
// chuyển đổi từ Wh sang kWh
float power = realTime.getPower() / 1000.0f;// convert to kWh
// chuyển từ mA sang A
float amperage = realTime.getAmperage() / 1000.0f;// convert to A
float voltage = realTime.getVoltage();
// cập nhật lên các biểu đồ
updateForChart(power, mPowerChart);
updateForChart(amperage, mAmperageChart);
updateForChart(voltage, mVoltageChart);
}
// phương thức này sẽ thêm giá trị value vào biểu đồ sau đó tịnh tiến biểu đồ về bên phải để tạo cảm giác như các đường biểu đồ đang chạy :|
private void updateForChart(float value, LineChart chart) {
// clear last warning info
chart.setNoDataText("");
chart.setNoDataTextDescription("");
LineData data = chart.getData();
DataSet dataSet = data.getDataSetByIndex(0);
data.addXValue("");
int xIndex = dataSet.getEntryCount() + VISIBLE_RANGE;
dataSet.addEntry(new Entry(value, xIndex));
chart.notifyDataSetChanged();
// limit visible count
chart.setVisibleXRangeMaximum(VISIBLE_RANGE);
chart.moveViewToX(data.getXValCount() - VISIBLE_RANGE - 1);
}
Các phương thức xử lý chính đã xong. Đây cũng là phần cuối cùng về xây dựng app android.Chốt
Như đã thấy thì ở phần này cũng khá đơn giản chỉ cần chú ý đến 1 số thứ như Handler, HttpURLConnection, BarChart, LineChart, Adapter và SharedPreferences.Về cơ bản thì công việc của chúng ta đã xong nhưng để demo được thì cần phải có phần thứ 3 là phần thiết bị vì nhờ chúng ta mới có được các giá trị real-time và thông tin điện năng(cái nay thì set cứng trong csdl để test cũng được). Để giải quyết vấn đề này thì mình sẽ thiết kế 1 virtual device để giả lập các thiết bị kết nối vào server. Phần tiếp theo hứa hẹn sẽ có nhiều điều thú vị để làm, các bạn cố gắng theo dỏi nhé :))
Mọi source code của project này được cập nhật tại github: https://github.com/xuanson33bk/iot-client-server
References
[1]. https://github.com/PhilJay/MPAndroidChart/wiki/Getting-Started[2]. http://developer.android.com/intl/vi/reference/java/net/HttpURLConnection.html
0 nhận xét :
Post a Comment