碼雲連接java
實現帶括號的四則運算
android
能夠輸入小數(".")、正負數(按"±"鍵)、對結果開根("√")、輸入上次計算的結果("Mr")、對算式和結果清零("CE")、退格("←")
git
處理異常,包括算式格式錯誤,缺失左右括號,除0錯誤等
算法
切換有理數模式和分數模式
數據庫
登陸、註冊(還沒有完成後端鏈接數據庫)
express
頁面佈局參考iPhone自帶的計算器,可是要實現括號按鈕,發現排不成好看的矩形。。因而多加了MR和開根的功能。
考慮到要知足有理數計算和分數計算,因此設計一個菜單來切換模式。同時分數的計算沒法處理浮點數,正好將小數點鍵改成/
。
順便作個登陸功能,計劃只有用戶成功登陸之後才能使用分數模式,目前還沒有完成。小程序
綜上,須要三個Activity,MainActivity實現計算器,LoginActivity實現登陸,RegisterActivity實現註冊。重點是MainActivity後端
清單文件以下:服務器
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="cn.edu.besti.is.onlinecalculator"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".LoginActivity" android:label="@string/login_screen_title" android:parentActivityName=".MainActivity">//ActionBar出現返回鍵,設置上一級界面 </activity> <activity android:name=".RegisterActivity" android:label="註冊" android:parentActivityName=".LoginActivity"> </activity> </application> <uses-permission android:name="android.permission.INTERNET" />//容許該應用程序連接網絡 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> </manifest>
佈局文件activity_main.xml
以下:
<?xml version="1.0" encoding="utf-8"?> <GridLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:useDefaultMargins="false" android:alignmentMode="alignBounds" android:columnOrderPreserved="false" android:layout_gravity="center_horizontal" android:background="#111" android:columnCount="4" android:rowCount="7" > <FrameLayout android:layout_width="match_parent" android:layout_height="200sp" android:layout_row="0" android:layout_column="3"> <TextView android:id="@+id/textView1" android:layout_width="match_parent" android:ellipsize="start" android:singleLine="true" android:gravity="center|start" android:layout_height="90sp" android:layout_gravity="center_horizontal" android:background="#111" android:text="" android:textAppearance="?android:attr/textAppearanceLarge" android:textColor="#fff" android:textSize="45sp" /> <TextView android:id="@+id/textView2" android:layout_width="match_parent" android:layout_height="110sp" android:layout_gravity="bottom" android:gravity="end|center" android:background="#000" android:text="" android:singleLine="true" android:textColor="#fff" android:textSize="60sp" /> </FrameLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_row="1" android:layout_column="3" android:layout_gravity="top" android:orientation="horizontal" > <Button android:id="@+id/button1_1" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1" android:layout_margin="3sp" android:textSize="45sp" android:text="CE" android:background="@drawable/button_style1" /> <Button android:id="@+id/button1_2" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1" android:layout_margin="3sp" android:textSize="45sp" android:background="@drawable/button_style1" android:text="±" /> <Button android:id="@+id/button1_3" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1" android:layout_margin="3sp" android:textSize="45sp" android:background="@drawable/button_style1" android:text="←" /> <Button android:id="@+id/button1_4" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1" android:layout_margin="3sp" android:textSize="45sp" android:textColor="#fff" android:background="@drawable/button_style2" android:text="√" /> </LinearLayout> ··· </GridLayout>
使用GridLayout配合LinearLayout和FrameLayout,FrameLayout包含兩個TextView,分別是用戶輸入的表達式和計算的結果。
每一個LinearLayout表明一行按鈕,不一樣的按鈕設置不同的樣式,以button_style1.xml
爲例:
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android" > <item android:state_pressed="true">//按下時的樣式 <shape android:shape="rectangle">//圓角按鈕 <solid android:color="#eee"/>//顏色 <corners android:radius="8dip"/>//圓角程度 </shape> </item> <item android:state_pressed="false">//鬆開時的樣式 <shape android:shape="rectangle"> <solid android:color="#bbb"/> <corners android:radius="8dip"/> </shape> </item> </selector>
主界面效果以下
其餘頁面的佈局見碼雲連接
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<String> 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<String> expr = new LinkedList<>();
來處理用戶的每次點擊形成的輸入,方便將數字和操做符分開,若是隊尾元素是數字或小數點,而當前值是數字或小數點,則對隊尾元素進行字符串拼接;若是隊尾元素是操做符,就直接入隊。最後計算結果的時候,依次拼接隊中元素,造成中綴表達式。
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應用默認不開啓網絡鏈接,要在清單文件裏聲明
<uses-permission android:name="android.permission.INTERNET" />
@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表明永遠摺疊在菜單中
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/option1" android:title="登陸" app:showAsAction="never" /> <item android:id="@+id/option2" android:title="分數模式" app:showAsAction="never"/> </menu>
個人搭檔定義了Client類來完成發送請求和收發數據,在這個過程當中進行加密傳輸。可是由於個人搭檔沒法解決文件權限的問題,因此我只能不通過文件來進行密鑰的傳輸。我在個人博客任務4的基礎上,修改代碼以下:
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);
服務器端簡單的用Java實現,一樣在個人博客任務4的基礎上修改代碼以下:
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); } } }
要實現ActionBar出現返回鍵,在清單文件中相應的Activity下設置parentActivityName
<activity android:name=".LoginActivity" android:label="@string/login_screen_title" android:parentActivityName=".MainActivity">//ActionBar出現返回鍵,設置上一級界面 </activity>
問題1解決:說明根本就沒有向我主機發送請求,原來Android程序中嘗試鏈接localhost,程序會將Android手機做爲主機,固然連不到我服務端所在的電腦。應該將地址改成內網地址
問題2解決(並無):沒有找到改權限的方法,因此只能直接傳輸密鑰,不通過文件。