一、团队成员
李怡龙 学号:1600802046 博客地址:https://www.cnblogs.com/lee-li/
刘显云 学号:1600802048 博客地址:https://www.cnblogs.com/lxy-y/
刘志祥 学号:1600802049 博客地址:https://www.cnblogs.com/love-love/
二、APK下载地址
Android:https://github.com/leeli73/Windroid/releases/download/1.0/Windroid.apk
PC:https://github.com/leeli73/Windroid_Server_PC/releases/download/1.0/Windroid_PC.exe
Server:https://github.com/leeli73/Windroid_Server_PC/releases/download/1.0/Windroid_Server.exe
三、项目地址
Android APP:https://github.com/leeli73/Windroid.git
Server PC:https://github.com/leeli73/Windroid_Server_PC.git
四、项目介绍
名称:Windroid
功能:主要用于共享Windows系统和Android手机的剪切板,用户不用在通过QQ、微信发信息给PC端,手机复制的信息可以共享给PC,PC复制的信息亦可以共享给手机。
主要构成:Windows端应用程序、Android端程序、Server端程序
4.1 团队项目的总体效果截图
Android登录界面
Android设置界面
PC登录界面
PC工作界面
PC端当登录成功后,便会自动隐藏窗口,在后台运行
Server工作界面
4.2 实现的功能及其效果的描述
登录
当用户输入用户名密码后,点击登录,验证通过后即可进入设置页面
点击注册后,即可进行注册
设置信息(目前测试有BUG、在某些情况下会闪退,比如快速上下滑动)
在点击允许修改的表项后,就会弹出下面的输入框
输入完成后,点击确定即可更新数据
五、项目中的关键代码
HTTP请求
使用OkHttp3库进行请求,主要用于登录、注册、数据交换
String url = "http://192.168.0.102:6888/SetData"; final OkHttpClient okHttpClient=new OkHttpClient(); RequestBody body = new FormBody.Builder() .add("UserID", Base64.encodeToString(StrUserID.getBytes(), Base64.DEFAULT)) .add("Data",Base64.encodeToString(Data.getBytes(),Base64.DEFAULT)) .build(); final Request request=new Request.Builder().url(url).post(body).build(); new Thread(new Runnable() { @Override public void run() { try { Response response=okHttpClient.newCall(request).execute(); if (response.isSuccessful()){ String body=response.body().string(); Log.i("test",body); } } catch (IOException e) { e.printStackTrace(); } } }).start();
listview生成
每行都有一个指定的view与其对应,方便修改数据等操作
AllInfo = findViewById(R.id.AllInfo); adapter = new BaseAdapter() { @Override public int getCount() { return 13; } @Override public Object getItem(int position) { return null; } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { LinearLayout linearLayout = new LinearLayout(body.this); TextView tital = new TextView(body.this); linearLayout.setOrientation(LinearLayout.VERTICAL); tital.setTextSize(25); switch (position) { case 0: tital.setText("用户信息"); tital.setGravity(LinearLayout.TEXT_ALIGNMENT_CENTER); tital.setTextSize(30); linearLayout.addView(tital); break; case 1: tital.setText("用户名ID"); linearLayout.addView(tital); linearLayout.addView(UserID); break; case 2: tital.setText("用户名"); linearLayout.addView(tital); linearLayout.addView(UserName); break; case 3: tital.setText("电子邮箱"); linearLayout.addView(tital); linearLayout.addView(Email); break; case 4: tital.setText("密码"); linearLayout.addView(tital); linearLayout.addView(PassWord); break; case 5: tital.setText("设置"); tital.setGravity(LinearLayout.TEXT_ALIGNMENT_CENTER); tital.setTextSize(30); linearLayout.addView(tital); break; case 6: tital.setText("最大数据长度/K"); linearLayout.addView(tital); linearLayout.addView(MaxDataLength); break; case 7: tital.setText("远程存储时间/s(<3600s)"); linearLayout.addView(tital); linearLayout.addView(RomoteDataSaveDate); break; case 8: tital.setText("本地存储时间/s(<3600s)"); linearLayout.addView(tital); linearLayout.addView(LocalDataSaveTime); break; case 9: tital.setText("局域网连接"); tital.setGravity(LinearLayout.TEXT_ALIGNMENT_CENTER); tital.setTextSize(30); linearLayout.addView(tital); break; case 10: tital.setText("自动扫描"); linearLayout.addView(tital); linearLayout.addView(LANAutoScan); break; case 11: tital.setText("局域网IP"); linearLayout.addView(tital); linearLayout.addView(LANIP); break; case 12: tital.setText("端口"); linearLayout.addView(tital); linearLayout.addView(LANPort); break; } return linearLayout; } }; AllInfo.setAdapter(adapter);
初始化数据
将asset目录下的配置读取并处理
try { InputStreamReader inputStreamReader = new InputStreamReader(getResources().getAssets().open("UserInfo.data")); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String line=""; while((line=bufferedReader.readLine())!=null) { String Temp[] = line.split(":"); if(Temp[0].equals("Username")) { StrUserName = new String(Temp[1]); } else if(Temp[0].equals("Password")) { StrPassWord = new String(Temp[1]); } } } catch (Exception e) { e.printStackTrace(); } try { InputStreamReader inputStreamReader = new InputStreamReader(getResources().getAssets().open("Setting.data")); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String line=""; while((line=bufferedReader.readLine())!=null) { String Temp[] = line.split(":"); if(Temp[0].equals("MaxDataLength")) { StrMaxDataLength = new String(Temp[1]); } else if(Temp[0].equals("LocalDataSaveTime")) { StrLocalDataSaveTime = new String(Temp[1]); } else if(Temp[0].equals("RemoteDataSaveTime")) { StrRomoteDataSaveDate = new String(Temp[1]); } else if(Temp[0].equals("UserID")) { StrUserID = new String(Temp[1]); } else if(Temp[0].equals("Email")) { StrEmail = new String(Temp[1]); } else if(Temp[0].equals("LANIP")) { StrLANIP = new String(Temp[1]); } else if(Temp[0].equals("LANPort")) { StrLANPort = new String(Temp[1]); } else if(Temp[0].equals("LANAutoScan")) { StrLANAutoScan = new String(Temp[1]); } } } catch (Exception e) { e.printStackTrace(); }
listview点击弹出提示
根据点击位置读取输入和判断是否允许修改
AllInfo.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, final int position, long id) { final EditText MyInput = new EditText(body.this); AlertDialog.Builder builder = new AlertDialog.Builder(body.this); builder.setTitle("请输入信息").setIcon(android.R.drawable.ic_dialog_alert).setView(MyInput).setNegativeButton("取消",null); builder.setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { switch (position) { case 0: //用户信息 break; case 1: //用户名ID Toast.makeText(body.this,"用户ID不允许修改",Toast.LENGTH_SHORT).show(); break; case 2: //用户名 Toast.makeText(body.this,"用户名不允许修改",Toast.LENGTH_SHORT).show(); break; case 3: //电子邮箱 StrEmail = MyInput.getText().toString(); Email.setText(StrEmail); Toast.makeText(body.this,"修改成功",Toast.LENGTH_SHORT).show(); break; case 4: //密码 StrPassWord = MyInput.getText().toString(); PassWord.setText(StrPassWord); Toast.makeText(body.this,"修改成功",Toast.LENGTH_SHORT).show(); break; case 5: //设置 break; case 6: //最大数据长度 StrMaxDataLength = MyInput.getText().toString(); MaxDataLength.setText(StrMaxDataLength); Toast.makeText(body.this,"修改成功",Toast.LENGTH_SHORT).show(); break; case 7: //远程存储时间/s(<3600s) StrRomoteDataSaveDate = MyInput.getText().toString(); RomoteDataSaveDate.setText(StrRomoteDataSaveDate); Toast.makeText(body.this,"修改成功",Toast.LENGTH_SHORT).show(); break; case 8: //本地存储时间/s(<3600s) StrLocalDataSaveTime = MyInput.getText().toString(); LocalDataSaveTime.setText(StrLocalDataSaveTime); Toast.makeText(body.this,"修改成功",Toast.LENGTH_SHORT).show(); break; case 9: //局域网连接 break; case 10: //自动扫描 StrLANAutoScan = MyInput.getText().toString(); LANAutoScan.setText(StrLANAutoScan); Toast.makeText(body.this,"修改成功",Toast.LENGTH_SHORT).show(); break; case 11: //局域网IP StrLANIP = MyInput.getText().toString(); LANIP.setText(StrLANIP); Toast.makeText(body.this,"修改成功",Toast.LENGTH_SHORT).show(); break; case 12: //端口 StrLANPort = MyInput.getText().toString(); LANPort.setText(StrLANPort); Toast.makeText(body.this,"修改成功",Toast.LENGTH_SHORT).show(); break; } } }); builder.show(); } });
本地剪切板监控线程(存在问题,完全按照官方API编写的读写剪切板,但是会闪退,API11之前和之后的方法全部尝试,依旧无法解决)
启动一个线程,循环监控本地剪切板
new Thread(new Runnable() { @Override public void run() { try { while (true) { String url = "http://192.168.0.102:6888/GetData"; final OkHttpClient okHttpClient=new OkHttpClient(); RequestBody body = new FormBody.Builder() .add("UserID", Base64.encodeToString(StrUserID.getBytes(), Base64.DEFAULT)) .build(); final Request request=new Request.Builder().url(url).post(body).build(); new Thread(new Runnable() { @Override public void run() { try { Response response=okHttpClient.newCall(request).execute(); if (response.isSuccessful()){ String body=response.body().string(); String Temp[] = body.split("@"); if(Temp[0].equals("New")) { /*//获取剪贴板管理器: ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); // 创建普通字符型ClipData ClipData mClipData = ClipData.newPlainText("Label", Temp[1]); // 将ClipData内容放到系统剪贴板里。 cm.setPrimaryClip(mClipData);*/ Log.i("test","Get New Data "+ Temp[1]); } else { Log.i("test","No New Data"); } } else { Log.i("test","No New Data"); } } catch (IOException e) { e.printStackTrace(); } } }).start(); Thread.sleep(1000); } } catch (Exception e) { e.printStackTrace(); } } }).start();
远程服务器获取数据线程(存在问题,完全按照官方API编写的读写剪切板,但是会闪退,API11之前和之后的方法全部尝试,依旧无法解决)
启动一个线程,循环监控远程剪切板
new Thread(new Runnable() { @Override public void run() { try { String OldData = ""; while (true) { Log.i("test","Set"); //ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); //String Data = cm.getText().toString().trim(); //Log.i("test",Data); String Data = "123"; if(!Data.equals(OldData)) { String url = "http://192.168.0.102:6888/SetData"; final OkHttpClient okHttpClient=new OkHttpClient(); RequestBody body = new FormBody.Builder() .add("UserID", Base64.encodeToString(StrUserID.getBytes(), Base64.DEFAULT)) .add("Data",Base64.encodeToString(Data.getBytes(),Base64.DEFAULT)) .build(); final Request request=new Request.Builder().url(url).post(body).build(); new Thread(new Runnable() { @Override public void run() { try { Response response=okHttpClient.newCall(request).execute(); if (response.isSuccessful()){ String body=response.body().string(); Log.i("test",body); } } catch (IOException e) { e.printStackTrace(); } } }).start(); } Thread.sleep(1000); } } catch (Exception e) { e.printStackTrace(); } } }).start();
更换返回键的功能为回到桌面
Intent intent = new Intent(); intent.setAction(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_HOME); startActivity(intent); Toast.makeText(body.this,"Windroid进入后台运行",Toast.LENGTH_SHORT).show();
六、心目中的前五名
1、季澈组 https://www.cnblogs.com/qingzhujushi/p/10200806.html
类似网易云音乐界面美观优雅的音乐播放器
项目优点:自动搜集本地音乐,有上一曲,下一曲,开始暂停,顺序播放,随机播放,单首播放,音量的控制,进度条,歌词,可以删除歌曲
项目缺点:无法加载云端的音乐,不能说是一个完美的音乐播放器。没有用户机制,无法保存自己的歌曲列表。
我的设想:支持播放云端的音乐,爬虫现有几个音乐播放器的资源。加入用户机制。
2、贺鸿琨组 https://www.cnblogs.com/hehongkun/p/10202262.html
类似QQ音乐界面,选择图片资源很优秀
项目优点:完成了歌曲列表与播放界面之间的切换,完成了播放过程中图片旋转状态与歌曲播放状态的绑定,还完成了歌曲进度条与歌曲进度的绑定。
项目缺点:无法加载云端的音乐,没有用户机制
我的设想:支持播放云端的音乐,爬虫现有几个音乐播放器的资源
3、李凌龙组 https://www.cnblogs.com/Trip1eL/p/10190488.html
对于我这样爱忘事者,这是一个刚需,简单使用
项目优点:功能齐全,界面简单
项目缺点:没有批量删除功能
我的设想:支持语音助手,一句话就能设定好
4、田光欣组 https://www.cnblogs.com/tiangxin/p/10206469.html
简单使用,没有花里胡哨功能的记事本
项目优点:界面简单,功能齐全
项目缺点:数据存储在本地,更换手机后无法使用
我的设想:支持语音助手,一句话记下文本,支持图片、音频、视频的记录。将数据加密存储在服务器上。
5、李钊组 https://www.cnblogs.com/18LZblog/p/10205321.html
功能齐全的乒乓球社区,乒乓球爱好者的必备
项目优点:功能齐全,无论是视频、排名、照片等都能一次性了解到
项目缺点:无法联网实时获取最新的数据
我的设想:APP自动去互联网上爬去最新的信息,向用户展示最新的数据
七、遇到的问题及解决方案
7.1 读写剪切板(任然未解决)
李怡龙 1600802046
使用最新的API,程序运行至此处会闪退
//获取剪贴板管理器: ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); // 创建普通字符型ClipData ClipData mClipData = ClipData.newPlainText("Label", Temp[1]); // 将ClipData内容放到系统剪贴板里。 cm.setPrimaryClip(mClipData);
换用老版API,程序依旧闪退
cm.setText()
7.2 乱码问题
李怡龙 1600802046
我们发现在传输中文的过程中,会出现乱码的问题,因为我们采用POST的形式,传输数据,如果有&等符号也会出现问题
所以我们决定对所有通讯过程中的数据进行BASE64编码
Android端
RequestBody body = new FormBody.Builder() .add("UserID", Base64.encodeToString(StrUserID.getBytes(), Base64.DEFAULT)) .build();
PC端
Base64 base64; Username = base64.encode(Username.toLatin1()); Password = base64.encode(Password.toLatin1());
Server端
RealUsernameBase64,err := base64.StdEncoding.DecodeString(Username[0]) if err != nil{ w.Write([]byte("error")) return } RealPasswordBase64,err := base64.StdEncoding.DecodeString(Password[0]) if err != nil{ w.Write([]byte("error")) return } RealUsername := string(RealUsernameBase64) RealPassword := string(RealPasswordBase64)
7.3 读取本地Asset目录下的配置文件
李怡龙 1600802046
因为我们存储的数据相对较少,而且不敏感,所以采用文本的形式存储
起初我们准备使用JSON的形式存储,但是在解析的JSON的过程中有部分问题,最后换用自定义格式的文本
InputStreamReader inputStreamReader = new InputStreamReader(getResources().getAssets().open("UserInfo.data")); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String line=""; while((line=bufferedReader.readLine())!=null) {}
7.4 Server中Redis的使用
李怡龙 1600802046
因为我们的数据存在一定的时效性,而且要求访问必须做到低延时,所以我们决定使用Redis 内存K-V型数据库
在查阅文档后,我们采用了"github.com/garyburd/redigo/redis"包进行redis的各类操作
并建立两张哈希表
第一张为用户信息表
_, err := RedisClient.Do("HMSET",Username,"UserID",UserID,"UserPassword",Password,"Email",Email,"PhoneNumber",PhoneNumber,"SaveTime",SaveTime) if err != nil { fmt.Println("redis hset error:", err) return false } else { //_,err := RedisClient.Do("expire","myKey","10") return true }
第二张为数据表
key :UserID value:Data
_,err1 := RedisClient.Do("HMSET",RealUserID,"Data",RealData) if err1 != nil { fmt.Println("redis hset error:", err) w.Write([]byte("SetError")) } else { //_,err := RedisClient.Do("expire","myKey","10") w.Write([]byte("SetSuccess")) }
7.5 因为技术上的问题,我们最早想要实现的局域网自动扫描没有编写出来,所以现在所有的功能必须经由服务器
八、分工
姓名 分工 工作比例 分数
李怡龙 服务器、PC端、安卓端POST、剪切板操作 50% 5
刘显云 安卓端登录UI设计、数据读取、存储 25% 2.5
刘志祥 安卓端listview设计及响应 25% 2.5
九、运行演示
启动Server及PC(Server实际运行于服务器,这里只是用于演示)
Server工作中交换数据的输出(实际工作中不输出,这里只用于演示效果)
Windroid安卓端
不知什么原因,登录界面的动画在模拟器中无法显示,所以这里在小米MIX(Android8.0)环境下演示
演示途中的黑屏,是应为调起小米安全键盘时,MIUI系统不允许录制该部分,所以自动进行了遮挡
博客园限制无法上传20M以上的图片,所以这里图片存于公共图床,可能加载相对较慢时点击这个链接前往腾讯云对象存储下载(流量贼啦贵,不到万不得已不要下)