目录
- 一、成果展示
- 二、实验内容及步骤
- 1、需求分析 & 页面布局
- 2、MainActivity
- 3、Client类
- 4、服务器端
- 5、登录、注册部分
- 三、遇到问题及解决
- 四、心得体会
- 五、码云链接
一、成果展示
码云链接
二、实验内容及步骤
1、需求分析 & 页面布局
页面布局参考iPhone自带的计算器,但是要实现括号按钮,发现排不成好看的矩形。。于是多加了MR和开根的功能。
考虑到要满足有理数计算和分数计算,所以设计一个菜单来切换模式。同时分数的计算无法处理浮点数,正好将小数点键改为/
。
顺便做个登录功能,计划只有用户成功登录以后才能使用分数模式,目前尚未完成。
综上,需要三个Activity,MainActivity实现计算器,LoginActivity实现登录,RegisterActivity实现注册。重点是MainActivity
清单文件如下
//ActionBar出现返回键,设置上一级界面
//允许该应用程序链接网络
布局文件activity_main.xml
如下
···
使用GridLayout配合LinearLayout和FrameLayout,FrameLayout包含两个TextView,分别是用户输入的表达式和计算的结果。
每个LinearLayout代表一行按钮,不同的按钮设置不一样的样式,以button_style1.xml
为例
- //按下时的样式
//圆角按钮
//颜色
//圆角程度
- //松开时的样式
其他页面的布局见码云链接
2、MainActivity
package cn.edu.besti.is.onlinecalculator;
import android.content.Intent;
import android.os.StrictMode;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import java.util.LinkedList;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button[] buttons = new Button[23];
private int[] ids = new int[]{
R.id.button1_1, R.id.button1_2, R.id.button1_3, R.id.button1_4,
R.id.button2_1, R.id.button2_2, R.id.button2_3, R.id.button2_4,
R.id.button3_1, R.id.button3_2, R.id.button3_3, R.id.button3_4,
R.id.button4_1, R.id.button4_2, R.id.button4_3, R.id.button4_4,
R.id.button5_1, R.id.button5_2, R.id.button5_3, R.id.button5_4,
R.id.button6_1, R.id.button6_2, R.id.button6_3
};
private TextView textView1, textView2;
private String result = "0";
private LinkedList expr = new LinkedList<>();
private String Mod = "Rational";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectDiskReads().detectDiskWrites().detectNetwork().penaltyLog().build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectLeakedSqlLiteObjects().detectLeakedClosableObjects().penaltyLog().penaltyDeath().build());
for (int i = 0; i < ids.length; i++) {
buttons[i] = findViewById(ids[i]);
buttons[i].setOnClickListener(this);
}
this.textView1 = findViewById(R.id.textView1);
this.textView2 = findViewById(R.id.textView2);
}
//onClick方法处理各种点击事件
@Override
public void onClick(View view) {
int id = view.getId();
Button button = view.findViewById(id);
String current = button.getText().toString();
String token;
StringBuilder expression = new StringBuilder();
if (current.equals("CE")) {
expr.clear();
result = "0";
} else if (current.equals("±")) {
if (!expr.isEmpty()) {
token = expr.pollLast();
if (!calcArithmatic.isOperator(token)) {
if (token.contains("-")) {
token = token.replaceAll("-", "");
} else {
token = "-" + token;
}
}
expr.offerLast(token);
}
} else if (current.equals("←")) {
expr.pollLast();
} else if (current.equals(".") || current.equals("/")) {
if (!expr.isEmpty()) {
token = expr.pollLast();
if (!calcArithmatic.isOperator(token)) {
if (!token.contains(current)) {
token += current;
}
}
expr.offerLast(token);
}
} else if (current.equals("=")) {//按下等号时,在本地将中缀表达式转为后缀表达式,传输给服务端,接收服务器的计算结果
if (!expr.isEmpty()) {
for (String s : expr) {
expression.append(" ").append(s);
}
try {
MyBC myBC = new MyBC();
final String formula = myBC.getEquation(expression.toString().trim());
try {
result = Client.Connect(formula, Mod);
} catch (Exception e) {
Toast.makeText(this, "请检查网络连接", Toast.LENGTH_SHORT).show();
}
} catch (ExprFormatException e) {
result = e.getMessage();
} catch (ArithmeticException e0) {
result = "Divide Zero Error";
} finally {
expr.clear();
}
}
} else if (current.equals("√")) {
if (Mod.equals("Rational")) {
result = String.valueOf(Math.sqrt(Double.parseDouble(result)));
}
} else if (current.equals("Mr")) {
if (result.matches("[0-9.\\-/]+")) {
current = result;
expr.offerLast(current);
}
} else if (calcArithmatic.isOperator(current)) {
expr.offerLast(current);
} else {
if (!expr.isEmpty()) {
token = expr.pollLast();
if (calcArithmatic.isOperator(token)) {
expr.offerLast(token);
expr.offer(current);
} else {
token += current;
expr.offerLast(token);
}
} else {
expr.offerLast(current);
}
}
for (String s : expr) {
expression.append(" ").append(s);
}
textView1.setText(expression.toString().trim());
textView2.setText(result);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.option1:
Intent intent = new Intent(this, LoginActivity.class);
startActivity(intent);
return true;
case R.id.option2:
expr.clear();
result = "";
if (item.getTitle().equals("分数模式")) {
buttons[21].setText("/");
item.setTitle("有理数模式");
Mod = "Fraction";
} else {
buttons[21].setText(".");
item.setTitle("分数模式");
Mod = "Rational";
}
return true;
default:
return super.onOptionsItemSelected(item);
}
}
}
使用
private LinkedList
来处理用户的每次点击造成的输入,方便将数字和操作符分开,如果队尾元素是数字或小数点,而当前值是数字或小数点,则对队尾元素进行字符串拼接;如果队尾元素是操作符,就直接入队。最后计算结果的时候,依次拼接队中元素,形成中缀表达式。expr = new LinkedList<>();
具体代码如下
package cn.edu.besti.is.onlinecalculator;
import java.util.EmptyStackException;
import java.util.Stack;
import java.util.StringTokenizer;
class MyBC extends calcArithmatic{
private Stack OpStack;
private String output="";
MyBC(){
OpStack = new Stack<>();
}
private void Shunt(String expr)throws ExprFormatException{
String token;
StringTokenizer tokenizer = new StringTokenizer(expr);
while (tokenizer.hasMoreTokens()){
token=tokenizer.nextToken();
if (isOperator(token)){
if (token.equals(")")){
try{
while (!OpStack.peek().equals("(")) {
output = output.concat(OpStack.pop() + " ");
}
OpStack.pop();
}catch (EmptyStackException e){
throw new ExprFormatException("Missing '('");
}
}
else if (!OpStack.empty()){
if(judgeValue(token)>judgeValue(OpStack.peek()) || token.equals("(")) {
OpStack.push(token);
}
else {
while (!OpStack.empty() && judgeValue(token)<=judgeValue(OpStack.peek())){
output=output.concat(OpStack.pop()+" ");
}
OpStack.push(token);
}
} else {
OpStack.push(token);
}
} else {
output=output.concat(token+" ");
}
}
while (!OpStack.empty()){
if (OpStack.peek().equals("(")){
throw new ExprFormatException("Missing ')'");
}
output=output.concat(OpStack.pop()+" ");
}
}
private int judgeValue(String str){
int value;
switch(str){
case "(":
value=1;
break;
case "+":
case "-":
value=2;
break;
case "×":
case "÷":
value=3;
break;
case ")":
value=4;
break;
default:
value=0;
}
return value;
}
String getEquation(String str) throws ExprFormatException{
Shunt(str);
return output;
}
}
- 输入等号后如下连接到客户端,捕获不同异常来输出不同结果,如果网络连接超时,弹出Toast提示
try {
MyBC myBC = new MyBC();
final String formula = myBC.getEquation(expression.toString().trim());
try {
result = Client.Connect(formula, Mod);
} catch (Exception e) {
Toast.makeText(this, "请检查网络连接",Toast.LENGTH_SHORT).show();
}
} catch (ExprFormatException e) {
result = e.getMessage();
} catch (ArithmeticException e0) {
result = "Divide Zero Error";
} finally {
expr.clear();
}
}
正常来说进行网络请求必须在线程中进行,但是因为我们只是一个小程序,阻塞一下没有什么问题,所以我就直接在主进程里面发送请求,需在MainActivity里面加上如下代码
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectDiskReads().detectDiskWrites().detectNetwork().penaltyLog().build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectLeakedSqlLiteObjects().detectLeakedClosableObjects().penaltyLog().penaltyDeath().build());
同时Android应用默认不开启网络连接,要在清单文件里声明
- 以下代码实现选择菜单功能
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.option1:
Intent intent = new Intent(this, LoginActivity.class);
startActivity(intent);
return true;
case R.id.option2:
expr.clear();
result = "";
if (item.getTitle().equals("分数模式")) {
buttons[21].setText("/");
item.setTitle("有理数模式");
Mod = "Fraction";
} else {
buttons[21].setText(".");
item.setTitle("分数模式");
Mod = "Rational";
}
return true;
default:
return super.onOptionsItemSelected(item);
}
}
点击切换模式以后,仅仅是将"."按钮的值改成"/",因为在处理点击事件的时候也是根据被点击按钮的值来决定行为的。
同时切换模式后相应的改变菜单里模式按钮的文字。
menu.xml如下,app:showAsAction="never"
决定该菜单按钮的位置,never代表永远折叠在菜单中
3、Client类
我定义了Client类来完成发送请求和收发数据,在这个过程中进行加密传输,代码如下
package cn.edu.besti.is.onlinecalculator;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.security.*;
import java.security.spec.X509EncodedKeySpec;
import java.net.*;
public class Client {
public static String Connect(String formula, String mod) throws Exception {
String mode = "AES";
Socket mysocket;
DataInputStream in;
DataOutputStream out;
mysocket = new Socket();
mysocket.connect(new InetSocketAddress("172.30.7.19", 2010),5000);
mysocket.setSoTimeout(5000);
in = new DataInputStream(mysocket.getInputStream());
out = new DataOutputStream(mysocket.getOutputStream());
//使用AES进行后缀表达式的加密
KeyGenerator kg = KeyGenerator.getInstance(mode);
kg.init(128);
SecretKey k = kg.generateKey();//生成密钥
byte[] mkey = k.getEncoded();
Cipher cp = Cipher.getInstance(mode);
cp.init(Cipher.ENCRYPT_MODE, k);
byte[] ptext = formula.getBytes("UTF8");
byte[] ctext = cp.doFinal(ptext);
//将加密后的后缀表达式传送给服务器
String out1 = B_H.parseByte2HexStr(ctext);
out.writeUTF(out1);
//创建客户端DH算法公、私钥
KeyPair keyPair = Key_DH5_6.createPubAndPriKey();
PublicKey pbk = keyPair.getPublic();//Client公钥
PrivateKey prk = keyPair.getPrivate();//Client私钥
//将公钥传给服务器
byte[] cpbk = pbk.getEncoded();
String CpubKey = B_H.parseByte2HexStr(cpbk);
out.writeUTF(CpubKey);
Thread.sleep(1000);
//接收服务器公钥
String SpubKey = in.readUTF();
byte[] spbk = H_B.parseHexStr2Byte(SpubKey);
KeyFactory kf = KeyFactory.getInstance("DH");
PublicKey serverPub = kf.generatePublic(new X509EncodedKeySpec(spbk));
//生成共享信息,并生成AES密钥
SecretKeySpec key = KeyAgree5_6.createKey(serverPub, prk);
//对加密后缀表达式的密钥进行加密,并传给服务器
cp.init(Cipher.ENCRYPT_MODE, key);
byte[] ckey = cp.doFinal(mkey);
String Key = B_H.parseByte2HexStr(ckey);
out.writeUTF(Key);
out.writeUTF(mod);
//接收服务器回答
return in.readUTF();
}
}
如下设置连接请求超时时间为5秒
mysocket.connect(new InetSocketAddress("172.30.7.19", 2010),5000);
如下设置收发数据超时时间为5秒
mysocket.setSoTimeout(5000);
密码学部分参考我搭档的博客
4、服务器端
服务器端简单的用Java实现,代码如下
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.*;
import java.security.spec.X509EncodedKeySpec;
public class Server extends Thread {
Socket socketOnServer;
public Server(Socket socketOnServer) {
super();
this.socketOnServer = socketOnServer;
}
public static void main(String[] args) {
ServerSocket serverForClient;
try {
serverForClient = new ServerSocket(2010);
while (true) {
System.out.println(currentThread()+"等待客户呼叫:");
Socket socketOnServer = serverForClient.accept();
new Server(socketOnServer).start();
}
} catch (IOException e1) {
System.out.println(e1.getMessage());
}
}
@Override
public void run() {
String mode = "AES";
DataOutputStream out = null;
DataInputStream in = null;
String result;
try {
out = new DataOutputStream(socketOnServer.getOutputStream());
in = new DataInputStream(socketOnServer.getInputStream());
//接收加密后的后缀表达式
String cformula = in.readUTF();
byte cipher[] = H_B.parseHexStr2Byte(cformula);
//接收Client端公钥
String push = in.readUTF();
byte np[] = H_B.parseHexStr2Byte(push);
KeyFactory kf = KeyFactory.getInstance("DH");
PublicKey ClientPub = kf.generatePublic(new X509EncodedKeySpec(np));
//创建服务器DH算法公、私钥
KeyPair keyPair = Key_DH5_6.createPubAndPriKey();
PublicKey pbk = keyPair.getPublic();//Server公钥
PrivateKey prk = keyPair.getPrivate();//Server私钥
//将服务器公钥传给Client端
byte cpbk[] = pbk.getEncoded();
String CpubKey = B_H.parseByte2HexStr(cpbk);
out.writeUTF(CpubKey);
Thread.sleep(1000);
//生成共享信息,并生成AES密钥
SecretKeySpec key = KeyAgree5_6.createKey(ClientPub, prk);
String k = in.readUTF();//读取加密后密钥
byte[] encryptKey = H_B.parseHexStr2Byte(k);
String mod = in.readUTF();
//对加密后密钥进行解密
Cipher cp = Cipher.getInstance(mode);
cp.init(Cipher.DECRYPT_MODE, key);
byte decryptKey[] = cp.doFinal(encryptKey);
//对密文进行解密
SecretKeySpec plainkey = new SecretKeySpec(decryptKey, mode);
cp.init(Cipher.DECRYPT_MODE, plainkey);
byte[] plain = cp.doFinal(cipher);
//计算后缀表达式结果
String formula = new String(plain);
MyDC myDC = new MyDC(mod);
try {
result = myDC.calculate(formula);
//后缀表达式formula调用MyDC进行求值
} catch (ExprFormatException e) {
result = e.getMessage();
} catch (ArithmeticException e0) {
result = "Divide Zero Error";
}
//将计算结果传给Client端
out.writeUTF(result);
} catch (Exception e) {
System.out.println("客户已断开" + e);
}
}
}
5、登录、注册部分
要实现ActionBar出现返回键,在清单文件中相应的Activity下设置parentActivityName
//ActionBar出现返回键,设置上一级界面
未完待续,随缘更新(这已经超出实验的范围了,我只是随便玩玩)
三、遇到问题及解决
- 问题1:测试时client尝试连接127.0.0.1一直连接不上,服务端一直在等待客户呼叫,没有任何反应。
问题1解决:说明根本就没有向我主机发送请求,原来Android程序中尝试连接localhost,程序会将Android手机作为主机,当然连不到我服务端所在的电脑。应该将地址改为内网地址
- 问题2:使用DH算法协商密钥时需要写入文件,但是Android虚拟手机没有写权限。。
问题2解决(并没有):没有找到改权限的方法,所以只能直接传输密钥,不经过文件。
问题3解决:Java浮点数除0会出现三种情况,NaN、Infinity、-Infinity,参考链接Java浮点数运算两个特殊的情况:NaN,Infinity。为了统一格式,我判断Infinity的情况然后主动抛出除零异常。
四、心得体会
虽然时间不是很充裕,但还是想熬夜敲代码,因为我不确定我到底有没有这个能力完成它,自然要挑战一下。因为没有系统地学过Android,很多地方都是现查现学,参考别人的代码改,总的来说我觉得最后做出来的东西还算比较满意。在这个过程中我更加深入的了解了Android的开发机制,学会了一些小技巧,一些组件的用法等等,同时对Java web编程也有了一定了解,我觉得Android其实和Web编程还是有相似之处的,希望之后能将数据库部分完成,“活学活用”一下。