安卓蓝牙4.0Socket通信传输图片
开发环境介绍:
- 开发工具:AndroidStudio 3.1.2
- 测试机:华为荣耀八青春版 安卓8.0(7.0) 红米note1S(4.4)
- SDK版本:28
- 项目最低支持安卓4.0版本
项目介绍:
-一台设备作为服务端,一台作为客户端,两台设备需要先蓝牙配对成功,然后才能开始打开APP进行操作。
- 一台手机作为服务端,右上方顶部ActionBar有一个按钮,点击后从本地图库添加图片和拍照获取图片。中间是GridView,再无其他布局。打开APP后自动开启Socket等待连接,与客户端连接上后,自动开始发送数据。
- 一台手机作为接收端,右上方顶部ActionBar有一个按钮,点击后从本地图库添加图片和拍照获取图片。底部有一个按钮,连接上服务端之后点击,即可开启读取数据的线程,获取服务器发送来的数据。
- 整个项目就一个Activity,想要安装服务端,就注释掉ClientActivity。客户端也是类似的操作。
- 所有显示的图片,支持单击全屏查看,可以手指操控放大缩小,长按图片会提示是否删除图片,点击图片名字,会提示更改图片名,弹出文本框。
- 由于文字聊天太简单,就没有发送文字的功能,后期有空的话,会做一个类似qq聊天的,内置小表情,可以发送本地图片。
- 同步增删,在查询结束后,客户端删除任意图片,服务端也跟着删除,客户端添加新图片,服务端也会自动添加
- 目前只支持300k以下图片慢传,修改每次传输的sleep方法的值可以实现以10倍速率传输20~30k以下的图
代码部分
服务器端
package com.kaytion.hgl.bluetoopicmanager;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.provider.MediaStore;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.kaytion.hgl.bluetoopicmanager.entity.BluetoothMsg;
import com.kaytion.hgl.bluetoopicmanager.entity.Contant;
import com.kaytion.hgl.bluetoopicmanager.entity.MediaBean;
import com.kaytion.hgl.bluetoopicmanager.entity.Pic;
import com.kaytion.hgl.bluetoopicmanager.util.Constant;
import com.kaytion.hgl.bluetoopicmanager.util.DBManager;
import com.kaytion.hgl.bluetoopicmanager.util.MySQLiteHelper;
import com.kaytion.hgl.bluetoopicmanager.view.ZoomImageView;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.UUID;
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
Context context;
public View contentView;
public PopupWindow popupWindow;
private MySQLiteHelper helper;
ZoomImageView ziv;
RelativeLayout zrl,choice;
ImageView hide,choicepic;
List<Pic> list;
LayoutInflater inflater;
LinearLayout bottom;
GridviewAdapter gridviewAdapter;
GridView gridView;
Bitmap get_bitmap;
Button queren;
int FLAG=0;//判断是否获取到了图片
String picName="0";
private Handler mHandler;
Boolean connectState=false;
private BluetoothServerSocket mServerSocket; // 服务端socket
private BluetoothAdapter mBluetoothAdapter; // Bluetooth适配器
private ServerThread mServerThread;//服务线程
private ReadThread mReadThread;//读取数据线程
public static final String PROTOCOL_SCHEME_RFCOMM = "btspp";// 服务器名称
private BluetoothSocket socket1; // 蓝牙socket对象
Queue<byte[]> qb=new LinkedList<>(); //一个用来装byte[]的队列,每装完一张图片的数据后,就会清空,遵循先进先出原则
List<MediaBean> mediaBeen = new ArrayList<>(); //装图片对象
private ArrayList<String> chatMsg;
@SuppressLint("HandlerLeak")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
context=this;
//控件绑定和初始化
initView();
initPopupWindow();
//设置点击监听事件
setListener();
//创建或打开数据库
createDB();
//查询数据库所有数据
SelectALL();
//判断页面是否有显示的内容
if(list.size()<1){
Toast.makeText(this,"您还没有录入过照片,快点击右上角添加图片吧!",Toast.LENGTH_SHORT).show();
}else {
//设置适配器,传递参数list,并显示图片和名字
gridviewAdapter=new GridviewAdapter(this,list);
gridView.setAdapter(gridviewAdapter);
}
mHandler=new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what==100) {
SelectALL();
//设置适配器,传递参数list,并显示图片和名字
gridviewAdapter = new GridviewAdapter(context, list);
gridView.setAdapter(gridviewAdapter);
}
}
};
}
//获取Handler实例对象
public Handler getHandler() {
return this.mHandler;
}
public void initView() {
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
//初始化helper
helper=DBManager.getIntance(this);
inflater= LayoutInflater.from(this);
ziv=(ZoomImageView) findViewById(R.id.zoomimageview_iv);
zrl=(RelativeLayout) findViewById(R.id.zoomimageview_rl);
//初始化zoomimageview,避免空指针异常
chuShi();
bottom=(LinearLayout) findViewById(R.id.bottom_rl);
choice=(RelativeLayout) findViewById(R.id.choice_rl);
choice.setVisibility(View.GONE);
hide=(ImageView) findViewById(R.id.hiden_iv);
choicepic=(ImageView) findViewById(R.id.choice_iv);
hide.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
zrl.setVisibility(View.GONE);
}
});
gridView=(GridView) findViewById(R.id.pic_gridview);
queren=(Button) findViewById(R.id.queren_btn);
chatMsg= new ArrayList<String>();// 初始化list,我们有需要的话,后期可以将数据以列表的形式进行显示
}
public void setListener(){
queren.setOnClickListener(this);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater=getMenuInflater();
inflater.inflate(R.menu.actionbar_menu,menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
showPopWindow();
return super.onOptionsItemSelected(item);
}
@Override
public void onClick(View view) {
switch (view.getId()){
//拍照
case R.id.open_from_camera:
Toast.makeText(this,"拍照",Toast.LENGTH_SHORT).show();
FLAG=1;
// 指定开启系统相机
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.CAMERA,Manifest.permission.WRITE_EXTERNAL_STORAGE},
1);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
startActivityForResult(intent,101);
hiddenWindow();
break;
//打开本地图库
case R.id.open_album:
Toast.makeText(this,"打开图库",Toast.LENGTH_SHORT).show();
FLAG=1;
Intent intent02 = new Intent();
/* 开启Pictures画面Type设定为image */
intent02.setType("image/*");
/* 使用Intent.ACTION_GET_CONTENT这个Action */
intent02.setAction(Intent.ACTION_GET_CONTENT);
/* 取得相片后返回本画面 */
startActivityForResult(intent02, 102);
hiddenWindow();
break;
// 取消
case R.id.cancel:
hiddenWindow();
break;
case R.id.queren_btn:
//保存图片和随机生成的名字到本地指定文件夹和SQLite,隐藏预览用的RelativeLayout
checkPic();
break;
}
}
//popupwindow界面设置
private void initPopupWindow() { //要在布局中显示的布局
contentView = LayoutInflater.from(this).inflate(R.layout.popupwindow_layout, null, false);
//实例化PopupWindow并设置宽高
popupWindow = new PopupWindow(contentView, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
popupWindow.setBackgroundDrawable(new BitmapDrawable());//点击外部消失,这里因为PopupWindow填充了整个窗口,所以这句代码就没用了
popupWindow.setOutsideTouchable(true); //设置可以点击
popupWindow.setTouchable(true); //进入退出的动画
popupWindow.setAnimationStyle(R.style.PopupAnimation);
popupWindow.setFocusable(true);//获取焦点,就置于试图最顶层,盖住了activity
TextView canmero=(TextView) contentView.findViewById(R.id.open_from_camera);
TextView album=(TextView) contentView.findViewById(R.id.open_album);
TextView cancle=(TextView) contentView.findViewById(R.id.cancel);
cancle.setOnClickListener(this);
canmero.setOnClickListener(this);
album.setOnClickListener(this);
}
//显示popwindow
private void showPopWindow() {
fitPopupWindowOverStatusBar(true);
View rootview = LayoutInflater.from(MainActivity.this).inflate(R.layout.activity_main,null);
popupWindow.showAtLocation(rootview, Gravity.BOTTOM, 0, 0);
}
//隐藏popupwindow
private void hiddenWindow(){
Toast.makeText(this,"隐藏popupwindow!",Toast.LENGTH_SHORT).show();
popupWindow.dismiss();
}
//popupwindow是否全屏显示,遮住其他控件
public void fitPopupWindowOverStatusBar(boolean needFullScreen) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
try { //利用反射重新设置mLayoutInScreen的值,当mLayoutInScreen为true时则PopupWindow覆盖全屏。
Field mLayoutInScreen = PopupWindow.class.getDeclaredField("mLayoutInScreen");
mLayoutInScreen.setAccessible(true);
mLayoutInScreen.set(popupWindow, needFullScreen);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
//ZoomImageView需要一个初始图片
public void chuShi() {
Bitmap bitmap = BitmapFactory.decodeResource(this.getResources(),R.drawable.defaults);
ziv.setImage(bitmap);
zrl.setVisibility(View.GONE);
}
//创建数据库
public void createDB(){
//getReadableDatabase()和getWritableDatabase()都是SQLiteOpenHelper类提供的(此处用的是其自定义子类的)用来创建或者打开SQLite数据库的
//区别在于,如果磁盘已满,getReadableDatabase()就是只有“只读”的功能
SQLiteDatabase db=helper.getWritableDatabase();//如果数据库存在,打开数据库;不存在,则先创建,再打开
db.close();
}
//修改数据
public void UpdatePicName(String picUrl,String name){
SQLiteDatabase db2=helper.getWritableDatabase();
String sql2="update "+ Constant.TABLE_NAME+" set "+Constant.NAME+"='"+name+"' where "+Constant.URL+"='"+picUrl+"'";
DBManager.execSQL(db2,sql2);
db2.close();
}
//删除数据
public void DeletePic(String picUrl){
SQLiteDatabase db3=helper.getWritableDatabase();
String sql3="delete from "+Constant.TABLE_NAME+" where "+Constant.URL+"='"+picUrl+"'";
DBManager.execSQL(db3,sql3);
db3.close();
}
//查询所有数据,插入list
public void SelectALL(){
SQLiteDatabase db=helper.getWritableDatabase();
String sql="select * from "+Constant.TABLE_NAME;
Cursor cursor=DBManager.SelectDataBySql(db,sql,null);
list=DBManager.cursorToList(cursor);
db.close();
//list已经获取到SQlite中所有数据
if(list.size()>0) {
Log.i("selectall", "" + list.get(0).getUrl());
}
}
//GridView的适配器
public class GridviewAdapter extends BaseAdapter {
Context contexts;
List<Pic> list2=new ArrayList<>();
private class Holder{
ImageView item_img;
TextView item_tex;
public ImageView getItem_img() {
return item_img;
}
public void setItem_img(ImageView item_img) {
this.item_img = item_img;
}
public TextView getItem_tex() {
return item_tex;
}
public void setItem_tex(TextView item_tex) {
this.item_tex = item_tex;
}
}
public GridviewAdapter(Context context,List<Pic> l) {
this.contexts=context;
this.list2=l;
}
@Override
public View getView(final int position, View view, ViewGroup viewGroup) {
Holder holder;
if(view==null){
view=inflater.inflate(R.layout.gridview_item_main,null);
holder=new Holder();
holder.item_img=(ImageView) view.findViewById(R.id.pic_iv);
holder.item_tex=(TextView)view.findViewById(R.id.name_tv);
//更换图片名
holder.item_tex.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
final EditText et3=new EditText(contexts);
new AlertDialog.Builder(contexts).setTitle("请输入新的图片名")
.setView(et3).setPositiveButton("确定",new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if(et3.getText().toString().length()<1||et3.getText().toString().length()>20){
Toast.makeText(contexts,"请输入1~20位字符数字组合!",Toast.LENGTH_SHORT).show();
}else{
UpdatePicName(list2.get(position).getUrl(),et3.getText().toString());
RefreshUI();
}
}
})
.setNegativeButton("取消", null).show();
UpdatePicName(list2.get(position).getUrl(),list2.get(position).getName());
}
});
//设置长按事件
holder.item_img.setOnLongClickListener(new bigClickListener(){
@Override
public boolean onLongClick(View view) {
//删除
Toast.makeText(contexts,"正在准备删除",Toast.LENGTH_SHORT).show();
AlertDialog alertDialog = new AlertDialog.Builder(contexts).setTitle("删除")
.setMessage("是否删除"+list2.get(position).getName())
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
//删除数据库记录
DeletePic(list2.get(position).getUrl());
//删除本地文件
deleteSingleFile(list2.get(position).getUrl());
RefreshUI();
}
}).setNegativeButton("取消",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
return;
}
}).create(); // 创建对话框
alertDialog.show(); // 显示对话框
return true;
}
});
holder.item_img.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//绝对路径转换为Bitmap类型图片
try {
//读取指定picName的图片转换为Bitmap类位图
FileInputStream localStream = null;
try {
localStream = openFileInput(list2.get(position).getName());
} catch (FileNotFoundException e) {
e.printStackTrace();
Toast.makeText(contexts,"没有在app安装目录data/data/找到指定名称的图片!",Toast.LENGTH_SHORT).show();
}
Bitmap bitmap = BitmapFactory.decodeStream(localStream);
ziv.setImage(bitmap);
zrl.setVisibility(View.VISIBLE);
}catch (NullPointerException npe){
npe.printStackTrace();
Toast.makeText(contexts,"要放大的图片不存在!空指针异常,请0. 检查当前安卓版本是否支持file路径读取!",Toast.LENGTH_SHORT).show();
}
Toast.makeText(contexts,"您点击了第"+(position+1)+"个imageView",Toast.LENGTH_SHORT).show();
}
});
view.setTag(holder);
}else{
holder=(Holder) view.getTag();
}
//读取指定picName的图片转换为Bitmap类位图
FileInputStream localStream = null;
try {
localStream = openFileInput(list2.get(position).getName());
} catch (FileNotFoundException e) {
e.printStackTrace();
Toast.makeText(contexts,"没有在app安装目录data/data/找到指定名字的图片!",Toast.LENGTH_SHORT).show();
}
Bitmap bitmap = BitmapFactory.decodeStream(localStream);
holder.item_img.setImageBitmap(bitmap);
holder.item_t