新建一個工程之后,我們可以先看到界面左邊的項目欄,我們可以看到,除了app目錄以外,大多數的文件和目錄都是自動生成的,我們也不需要對他們進行修改,而app目錄之下的文件才是我們工作的重點。下面,我先對app目錄下的內容進行一些講解。
1.AndroidManifest.xml
這是整個項目的配置文件,我們在程序中定義的四大組件都需要在這里注冊,另外,也可以在這里給應用程序添加權限聲明。
2.java
這個是放置我們所有java代碼的地方。
3.res
這個項目中所使用到的所有圖片、布局、字符串資源都要存放在這個項目。其中,drawable文件夾和mipmap文件夾都是用來存放圖片資源的。layout文件夾是用來存放布局資源的,values文件夾是存放字符串等資源的。
現在,我開始介紹一下關於android 藍牙的通信的知識。
首先,我們要在一個頁面中打開和關閉藍牙。我們可以點擊layout下的布局文件,先在其中添加兩個按鈕。在布局文件中,我們可以使用兩種方法來調出兩個按鈕。第一種就是直接在design界面,點擊Palette,找到Button,點擊然后拖動到旁邊的界面上。第二種就是在Text界面直接打入代碼,例如:
1 <Button
android:id="@+id/bt" 2 android:layout_width="match_parent" 3 android:layout_height="wrap_content" 4 tools:ignore="MissingConstraints" />
其中,layout_width和layout_height是必須要有的屬性,設置為match_parent表示充滿父控件空間,wrap_content表示根據控件自身大小顯示。id則是這個控件的名稱。然后我們開始設置這個按鈕的點擊事件。在我們的java文件中先定義一個Button對象,然后使用Button的對象調用findViewId方法。方法里的參數為控件的id。然后就設置點擊事件。
button.findViewById(R.id.bt); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { } });
在onClick方法中添加事件的代碼即可。
藍牙的開啟
首先,要在新建項目中的AndroidManifest.xml中聲明兩個權限:BLUETOOTH權限和BLUETOOTH_ADMIN權限。其中,BLUETOOTH權限用於請求連接和傳送數據;BLUETOOTH_ADMIN權限用於啟動設備、發現或進行藍牙設置,如果要擁有該權限,必須現擁有BLUETOOTH權限。
因為android 6.0之后采用新的權限機制來保護用戶的隱私,如果我們設置的targetSdkVersion大於或等於23,則需要另外添加ACCESS_COARSE_LOCATION和ACCESS_FINE_LOCATION權限,否則,可能會出現搜索不到藍牙設備的問題。
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> <uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>
然后要在Activity中,添加
BluetoothAdapter blueadapter = BluetoothAdapter.getDefaultAdapter(); //獲取藍牙適配器
來獲取藍牙適配器的對象
在按鈕的點擊事件中添加以下代碼來開啟藍牙
if(blueadapter==bull) //表示手機不支持藍牙 return;
if (!blueadapter.isEnabled()) //判斷本機藍牙是否打開 {//如果沒打開,則打開藍牙 blueadapter.enable(); }
同理,使用disable()可以關閉藍牙。
搜索藍牙
接下來是搜索藍牙設備,如果想要改變自己藍牙能否被搜索的狀態,可以使用以下的代碼來使自己的藍牙設備可被發現
if (blueadapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) //不在可被搜索的范圍 { Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);//設置本機藍牙在300秒內可見 startActivity(discoverableIntent); }
如果想搜索別人的設備,就可以在按鈕的點擊事件中調用startDiscover()來搜索藍牙
public void doDiscovry() { if (blueadapter.isDiscovering()) { //判斷藍牙是否正在掃描,如果是調用取消掃描方法;如果不是,則開始掃描 blueadapter.cancelDiscovery(); } else blueadapter.startDiscovery(); }
我們要想獲得搜索到的結果,就需要注冊廣播
首先,要定義一個列表控件listview和一個集合適配器和兩個ArrayList集合類的對象,這兩個對象都是用來存設備的地址,只是有一個不是用來顯示出來的。在主類中定義以下的集合適配器和兩個ArrayList集合類的對象,然后在onCreate中定義適配器
public ArrayAdapter adapter; ListView listView = (ListView) findViewById(R.id.list);//控件 列表
//定義一個列表,存藍牙設備的地址。 public ArrayList<String> arrayList=new ArrayList<>(); //定義一個列表,存藍牙設備地址,用於顯示。 public ArrayList<String> deviceName=new ArrayList<>();
//定義適配器
adapter = new ArrayAdapter(this, android.R.layout.simple_expandable_list_item_1, deviceName);
//給列表添加適配器
listView.setAdapter(adapter);
然后定義廣播以及處理廣播的消息
IntentFilter intentFilter = new IntentFilter(BluetoothDevice.ACTION_FOUND);//注冊廣播接收信號 registerReceiver(bluetoothReceiver, intentFilter);//用BroadcastReceiver 來取得結果 private final BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (BluetoothDevice.ACTION_FOUND.equals(action)) { BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); deviceName.add("設備名:"+device.getName()+"\n" +"設備地址:"+device.getAddress() + "\n");//將搜索到的藍牙名稱和地址添加到列表。 arrayList.add( device.getAddress());//將搜索到的藍牙地址添加到列表。 adapter.notifyDataSetChanged();//更新 } } };
搜索完設備后,要記得注銷廣播。注冊后的廣播對象在其他地方有強引用,如果不取消,activity會釋放不了資源 。
protected void onDestroy(){ super.onDestroy();//解除注冊 unregisterReceiver(bluetoothReceiver); }
4.了解targetSdkVersion是否大於或等於23
若是大於或等於23,除了添加了藍牙權限外,還要動態獲取位置權限,才能將搜索到的藍牙設備顯示出來。若是小於,則不需要動態獲取權限。
動態申請權限
public void applypermission() { if (Build.VERSION.SDK_INT >= 23) { //檢查是否已經給了權限 int checkpermission = ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_FINE_LOCATION); if (checkpermission != PackageManager.PERMISSION_GRANTED) {//沒有給權限 Log.e("permission", "動態申請"); //參數分別是當前活動,權限字符串數組,requestcode ActivityCompat.requestPermissions(WiFiMainActivity.this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 1); } } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { Toast.makeText(WiFiMainActivity.this, "已授權", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(WiFiMainActivity.this, "拒絕授權", Toast.LENGTH_SHORT).show(); } }
配對藍牙設備
藍牙的配對和連接有兩種方式。一種是每個設備作為一個客戶端去連接一個服務端,向對方發起連接。另一種則是作為服務端來接收客戶端發來連接的消息。藍牙之間的數據傳輸采用的是和TCP傳輸類似的傳輸機制。
1.作為客戶端連接
首先要獲取一個代表遠程設備BluetoothDevice的對象,然后使用該BluetoothDevice的對象來獲取一個BluetoothSocket對象。BluetoothSocket對象調用connect()可以建立連接。
藍牙連接整個過程需要在子線程中執行的,並且要將 scoket.connect()放在一個新的子線程中,因為如果將這個方法也放在同一個子線程中解決的話,就會永遠報錯read failed, socket might closed or timeout, read ret: -1;借鑒網上的方法:再開一個子線程專門執行socket.connect()方法,問題可以解決;
另外,借鑒網上方法和建議,在獲得socket的時候 ,盡量不使用uuid方式;因為這樣雖然能夠獲取到socket 但是不能進行自動,所以使用的前提是已經配對了的設備連接;
使用反射的方式,能夠自動提示配對,也適合手機間通信。
首先,在設置列表中顯示的藍牙的點擊事件,其中ClientThread是連接,發送和接收的線程類
//定義列表Item的點擊事件 listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) { BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); device = adapter.getRemoteDevice(arrayList.get(i)); mge = editText.getText().toString() + "\r\n";//獲得編輯文本框里的文字 clientThread = new ClientThread(device, mge, context); clientThread.start(); } });
然后定義一個線程類,在他的構造方法中接收listview點擊事件里傳來的參數
class ClientThread extends Thread { public void run() { } }
在run方法中輸入以下代碼
final BluetoothSocket socket = (BluetoothSocket) device.getClass().getDeclaredMethod("createRfcommSocket", new Class[]{int.class}).invoke(device, 1);
代碼中的device需要把注冊廣播時的device作為參數傳進線程中。注意,傳進來的device的值要為遠程設備的地址,若不是或有出入,則可能會出現NullPointerException異常,並提示嘗試調用一個空的對象。為了解決這個問題,可以把顯示獲得的device名字、地址和傳入線程的device的地址分在不同的集合類。傳入線程的device使用只有設備地址的集合類。
在連接藍牙之前,還要先取消藍牙設備的掃描,否則容易連接失敗。
adapter.cancelDiscovery();//adapter為獲取到的藍牙適配器 socket.connect();//連接
2.作為服務端連接
服務端接收連接需要使用BluetoothServerSocket類,它的作用是監聽進來的連接,在一個連接被接收之后,會返回一個BluetoothSocket對象,這個對象可以用來和客戶端進行通信。
與客戶端一樣,服務端也要在子線程中實現。通過調用listenUsingRfcommWithServiceRecord(String,UUID)方法可以得到一個BluetoothServerSocket的對象,然后再用這個對象來調用accept()來返回一個BluetoothSocket對象。由於accept()是個阻塞的方法,它會直到接收到一個連接或異常之后才會返回,所以要放在子線程中。
bluetoothServerSocket=bluetoothAdapter.listenUsingRfcommWithServiceRecord(bluetoothAdapter.getDefaultAdapter().getName(), UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"));
//bluetoothServerSocket= (BluetoothServerSocket) bluetoothAdapter.getClass().getMethod("listenUsingRfcommOn",new Class[]{int.class}).invoke(bluetoothAdapter,10); socket=bluetoothServerSocket.accept();//接收連接
這個連接時只允許一個客戶端連接,因此在BluetoothServerSocket對象接收到一個連接請求時就要立刻調用close()方法把服務端關閉。
當兩個設備成功連接之后,雙方都會有一個BluetoothSocket對象,這時,就可以在設備之間傳送數據了。
1.使用getOutputStream()方法來獲取輸出流來處理傳輸。
2.調用write()。
os = socket.getOutputStream();//獲取輸出流 if (os != null) {//判斷輸出流是否為空 os.write(message.getBytes("UTF-8")); } os.flush();//將輸出流的數據強制提交 os.close();//關閉輸出流 }
3.使用getInputStream()方法來獲取輸出流來處理傳輸。
4.創建一個新的線程來read()輸入流,這里是接收16進制數然后轉換為10進制數顯示
ew Thread(new Runnable() { @Override public void run() { try { while (true) { int count = 0; while (count == 0) { count = is.available(); } byte buf[] = new byte[count]; if (buf != null) { is.read(buf); // BuletoothMainActivity.num++; String message = BuletoothMainActivity.bytesToIntString(buf); BuletoothMainActivity.UpdateRevMsg(message); } } } catch (IOException e) { e.printStackTrace(); } } }).start();
public static String bytesToIntString(byte[] bytes) {//16進制轉10進制以字符串形式輸出顯示 String result = ""; for (int i = 0; i < bytes.length; i++) { String hexString = Integer.toString(bytes[i] & 0xFF); if (hexString.length() == 1) { hexString = '0' + hexString; } result += hexString.toUpperCase(); } return result; }
public static void UpdateRevMsg(String revMsg) { mRevMsg=revMsg; handler.post(RefreshTextView); } private static Runnable RefreshTextView=new Runnable() { @Override public void run() { textView2.setText(mRevMsg); } };
在服務端發送和接收也類似上面。