diff --git a/res/layout/actionbar_progress_indeterminate.xml b/res/layout/actionbar_progress_indeterminate.xml
new file mode 100644
index 0000000..ba1c316
--- /dev/null
+++ b/res/layout/actionbar_progress_indeterminate.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/list_item_device.xml b/res/layout/list_item_device.xml
new file mode 100644
index 0000000..b7d0e2d
--- /dev/null
+++ b/res/layout/list_item_device.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/menu/main.xml b/res/menu/main.xml
index c002028..0aa7c7b 100644
--- a/res/menu/main.xml
+++ b/res/menu/main.xml
@@ -1,9 +1,35 @@
+
+
+
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index e96ba21..b412ef1 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -9,5 +9,12 @@
Not supported
Supported
No data
+ Unknown Device
+
+
+ Connect
+ Disconnect
+ Scan
+ Stop
\ No newline at end of file
diff --git a/src/uk/co/alt236/btlescan/MainActivity.java b/src/uk/co/alt236/btlescan/MainActivity.java
index b04131a..55a85bf 100644
--- a/src/uk/co/alt236/btlescan/MainActivity.java
+++ b/src/uk/co/alt236/btlescan/MainActivity.java
@@ -2,6 +2,7 @@ package uk.co.alt236.btlescan;
import java.util.Collection;
+import uk.co.alt236.btlescan.adapters.LeDeviceListAdapter;
import uk.co.alt236.btlescan.containers.AdRecord;
import uk.co.alt236.btlescan.containers.AdRecordUtils;
import uk.co.alt236.btlescan.containers.BluetoothLeDevice;
@@ -13,6 +14,7 @@ import android.bluetooth.BluetoothDevice;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
+import android.view.MenuItem;
import android.widget.TextView;
import butterknife.ButterKnife;
import butterknife.InjectView;
@@ -20,12 +22,11 @@ import butterknife.InjectView;
public class MainActivity extends ListActivity {
@InjectView(R.id.tvBluetoothLe) TextView mTvBluetoothLeStatus;
@InjectView(R.id.tvBluetoothStatus) TextView mTvBluetoothStatus;
-
- private static final long SCAN_PERIOD = 10000;
+
private BluetoothUtils mBluetoothUtils;
private BluetoothLeScanner mScanner;
- //private LeDeviceListAdapter mLeDeviceListAdapter;
-
+ private LeDeviceListAdapter mLeDeviceListAdapter;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -35,62 +36,93 @@ public class MainActivity extends ListActivity {
mScanner = new BluetoothLeScanner(mLeScanCallback, mBluetoothUtils);
}
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- // Inflate the menu; this adds items to the action bar if it is present.
- getMenuInflater().inflate(R.menu.main, menu);
- return true;
- }
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.main, menu);
+ if (!mScanner.isScanning()) {
+ menu.findItem(R.id.menu_stop).setVisible(false);
+ menu.findItem(R.id.menu_scan).setVisible(true);
+ menu.findItem(R.id.menu_refresh).setActionView(null);
+ } else {
+ menu.findItem(R.id.menu_stop).setVisible(true);
+ menu.findItem(R.id.menu_scan).setVisible(false);
+ menu.findItem(R.id.menu_refresh).setActionView(R.layout.actionbar_progress_indeterminate);
+ }
+ return true;
+ }
-
-
-
-
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_scan:
+ mLeDeviceListAdapter.clear();
+ mScanner.scanLeDevice(-1, true);
+ invalidateOptionsMenu();
+ break;
+ case R.id.menu_stop:
+ mScanner.scanLeDevice(-1, false);
+ invalidateOptionsMenu();
+ break;
+ }
+ return true;
+ }
+
private BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() {
- @Override
- public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
-
- final BluetoothLeDevice deviceLe = new BluetoothLeDevice(device, rssi, scanRecord);
- Log.d("TAG", "~ New BT Device: " + deviceLe);
-
- final Collection adRecords = deviceLe.getAdRecordStore().getRecordsAsCollection();
-
- for(final AdRecord record : adRecords){
- Log.d("TAG", "~ Has Record: " + record.getType() + ": '" + record.getHumanReadableType() +"', data: '"+ AdRecordUtils.getRecordDataAsString(record) + "'");
- }
-
-// runOnUiThread(new Runnable() {
-// @Override
-// public void run() {
-// mLeDeviceListAdapter.addDevice(device);
-// mLeDeviceListAdapter.notifyDataSetChanged();
-// }
-// });
-
- }
+ @Override
+ public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
+
+ final BluetoothLeDevice deviceLe = new BluetoothLeDevice(device, rssi, scanRecord);
+ Log.d("TAG", "~ New BT Device: " + deviceLe);
+
+ final Collection adRecords = deviceLe.getAdRecordStore().getRecordsAsCollection();
+
+ for(final AdRecord record : adRecords){
+ Log.d("TAG", "~ Has Record: " + record.getType() + ": '" + record.getHumanReadableType() +"', data: '"+ AdRecordUtils.getRecordDataAsString(record) + "'");
+ }
+
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mLeDeviceListAdapter.addDevice(deviceLe);
+ mLeDeviceListAdapter.notifyDataSetChanged();
+ }
+ });
+
+ }
};
-
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mScanner.scanLeDevice(-1, false);
+ mLeDeviceListAdapter.clear();
+ }
+
@Override
public void onResume(){
super.onResume();
final boolean mIsBluetoothOn = mBluetoothUtils.isBluetoothOn();
final boolean mIsBluetoothLePresent = mBluetoothUtils.isBluetoothLeSupported();
-
+
if(mIsBluetoothOn){
mTvBluetoothStatus.setText(R.string.on);
} else {
mTvBluetoothStatus.setText(R.string.off);
}
-
+
if(mIsBluetoothLePresent){
mTvBluetoothLeStatus.setText(R.string.supported);
} else {
mTvBluetoothLeStatus.setText(R.string.not_supported);
}
-
+
+ mLeDeviceListAdapter = new LeDeviceListAdapter(this);
+ setListAdapter(mLeDeviceListAdapter);
+
mBluetoothUtils.askUserToEnableBluetoothIfNeeded();
if(mIsBluetoothOn && mIsBluetoothLePresent){
- mScanner.scanLeDevice(true);
+ mScanner.scanLeDevice(-1, true);
+ invalidateOptionsMenu();
}
}
diff --git a/src/uk/co/alt236/btlescan/adapters/LeDeviceListAdapter.java b/src/uk/co/alt236/btlescan/adapters/LeDeviceListAdapter.java
new file mode 100644
index 0000000..e05fe59
--- /dev/null
+++ b/src/uk/co/alt236/btlescan/adapters/LeDeviceListAdapter.java
@@ -0,0 +1,92 @@
+package uk.co.alt236.btlescan.adapters;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import uk.co.alt236.btlescan.R;
+import uk.co.alt236.btlescan.containers.BluetoothLeDevice;
+import android.app.Activity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+ // Adapter for holding devices found through scanning.
+ public class LeDeviceListAdapter extends BaseAdapter {
+ private final List mLeDevices;
+ private final LayoutInflater mInflator;
+
+ public LeDeviceListAdapter(Activity activity) {
+ super();
+ mLeDevices = new ArrayList();
+ mInflator = activity.getLayoutInflater();
+ }
+
+ public void addDevice(BluetoothLeDevice device) {
+ final int position = mLeDevices.indexOf(device);
+ if(position == -1){
+ mLeDevices.add(device);
+ } else {
+ mLeDevices.set(position, device);
+ }
+ }
+
+ public BluetoothLeDevice getDevice(int position) {
+ return mLeDevices.get(position);
+ }
+
+ public void clear() {
+ mLeDevices.clear();
+ }
+
+ @Override
+ public int getCount() {
+ return mLeDevices.size();
+ }
+
+ @Override
+ public Object getItem(int i) {
+ return mLeDevices.get(i);
+ }
+
+ @Override
+ public long getItemId(int i) {
+ return i;
+ }
+
+ @Override
+ public View getView(int i, View view, ViewGroup viewGroup) {
+ ViewHolder viewHolder;
+ // General ListView optimization code.
+ if (view == null) {
+ view = mInflator.inflate(R.layout.list_item_device, null);
+ viewHolder = new ViewHolder();
+ viewHolder.deviceAddress = (TextView) view.findViewById(R.id.device_address);
+ viewHolder.deviceName = (TextView) view.findViewById(R.id.device_name);
+ viewHolder.deviceRssi = (TextView) view.findViewById(R.id.device_rssi);
+ view.setTag(viewHolder);
+ } else {
+ viewHolder = (ViewHolder) view.getTag();
+ }
+
+ final BluetoothLeDevice device = mLeDevices.get(i);
+ final String deviceName = device.getName();
+
+ if (deviceName != null && deviceName.length() > 0){
+ viewHolder.deviceName.setText(deviceName);
+ } else{
+ viewHolder.deviceName.setText(R.string.unknown_device);
+ }
+
+ viewHolder.deviceAddress.setText(device.getAddress());
+ viewHolder.deviceRssi.setText(String.valueOf(device.getRssi()) + "db");
+ return view;
+ }
+
+ static class ViewHolder {
+ TextView deviceName;
+ TextView deviceAddress;
+ TextView deviceRssi;
+ }
+
+ }
\ No newline at end of file
diff --git a/src/uk/co/alt236/btlescan/containers/AdRecord.java b/src/uk/co/alt236/btlescan/containers/AdRecord.java
index c4a4d88..443052f 100644
--- a/src/uk/co/alt236/btlescan/containers/AdRecord.java
+++ b/src/uk/co/alt236/btlescan/containers/AdRecord.java
@@ -7,6 +7,26 @@ import java.util.Arrays;
* AdRecord
*/
public final class AdRecord {
+
+ // 02 # Number of bytes that follow in first AD structure
+ // 01 # Flags AD type
+ // 1A # Flags value 0x1A = 000011010
+ // bit 0 (OFF) LE Limited Discoverable Mode
+ // bit 1 (ON) LE General Discoverable Mode
+ // bit 2 (OFF) BR/EDR Not Supported
+ // bit 3 (ON) Simultaneous LE and BR/EDR to Same Device Capable (controller)
+ // bit 4 (ON) Simultaneous LE and BR/EDR to Same Device Capable (Host)
+ // 1A # Number of bytes that follow in second (and last) AD structure
+ // FF # Manufacturer specific data AD type
+ // 4C 00 # Company identifier code (0x004C == Apple)
+ // 02 # Byte 0 of iBeacon advertisement indicator
+ // 15 # Byte 1 of iBeacon advertisement indicator
+ // e2 c5 6d b5 df fb 48 d2 b0 60 d0 f5 a7 10 96 e0 # iBeacon proximity uuid
+ // 00 00 # major
+ // 00 00 # minor
+ // c5 # The 2's complement of the calibrated Tx Power
+
+
/**
* General FLAGS
*
@@ -143,7 +163,7 @@ public final class AdRecord {
case TYPE_MANUFACTURER_SPECIFIC_DATA:
return "Manufacturer Specific Data";
case TYPE_LOCAL_NAME_COMPLETE:
- return "Name";
+ return "Name (Complete)";
case TYPE_LOCAL_NAME_SHORT:
return "Name (Short)";
case TYPE_SECURITY_MANAGER_OOB_FLAGS:
diff --git a/src/uk/co/alt236/btlescan/containers/AdRecordUtils.java b/src/uk/co/alt236/btlescan/containers/AdRecordUtils.java
index fe84c9d..8b57309 100644
--- a/src/uk/co/alt236/btlescan/containers/AdRecordUtils.java
+++ b/src/uk/co/alt236/btlescan/containers/AdRecordUtils.java
@@ -11,6 +11,25 @@ import android.annotation.SuppressLint;
public class AdRecordUtils {
/* Helper functions to parse out common data payloads from an AD structure */
+ static final String HEXES = "0123456789ABCDEF";
+
+ public static String byteArrayToHexString(final byte[] array){
+ final StringBuffer sb = new StringBuffer();
+ boolean firstEntry = true;
+ sb.append('[');
+
+ for ( final byte b : array ) {
+ if(!firstEntry){
+ sb.append(", ");
+ }
+ sb.append(HEXES.charAt((b & 0xF0) >> 4));
+ sb.append(HEXES.charAt((b & 0x0F)));
+ firstEntry = false;
+ }
+
+ sb.append(']');
+ return sb.toString();
+ }
public static String getRecordDataAsString(AdRecord nameRecord) {
if(nameRecord == null){return new String();}
@@ -64,7 +83,7 @@ public class AdRecordUtils {
return Collections.unmodifiableList(records);
}
-
+
@SuppressLint("UseSparseArrays")
public static Map parseScanRecordAsMap(byte[] scanRecord) {
final Map records = new HashMap();
diff --git a/src/uk/co/alt236/btlescan/containers/BluetoothLeDevice.java b/src/uk/co/alt236/btlescan/containers/BluetoothLeDevice.java
index bf82a57..30803c7 100644
--- a/src/uk/co/alt236/btlescan/containers/BluetoothLeDevice.java
+++ b/src/uk/co/alt236/btlescan/containers/BluetoothLeDevice.java
@@ -7,7 +7,7 @@ import android.bluetooth.BluetoothDevice;
public class BluetoothLeDevice {
private final BluetoothDevice mDevice;
- private final int mRssi;
+ private transient final int mRssi;
private final byte[] mScanRecord;
private final AdRecordStore mRecordStore;
@@ -18,14 +18,37 @@ public class BluetoothLeDevice {
mRecordStore = new AdRecordStore(AdRecordUtils.parseScanRecordAsMap(scanRecord));
}
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ BluetoothLeDevice other = (BluetoothLeDevice) obj;
+ if (mDevice == null) {
+ if (other.mDevice != null)
+ return false;
+ } else if (!mDevice.equals(other.mDevice))
+ return false;
+ if (!Arrays.equals(mScanRecord, other.mScanRecord))
+ return false;
+ return true;
+ }
+
+ public String getAddress(){
+ return mDevice.getAddress();
+ }
+
public AdRecordStore getAdRecordStore(){
return mRecordStore;
}
-
+
public String getBluetoothDeviceBondState(){
return resolveBondingState(mDevice.getBondState());
}
-
+
public String getBluetoothDeviceClassName(){
return resolveBluetoothClass(mDevice.getBluetoothClass().getDeviceClass());
}
@@ -33,7 +56,11 @@ public class BluetoothLeDevice {
public BluetoothDevice getDevice() {
return mDevice;
}
-
+
+ public String getName(){
+ return mDevice.getName();
+ }
+
public int getRssi() {
return mRssi;
}
@@ -41,11 +68,22 @@ public class BluetoothLeDevice {
public byte[] getScanRecord() {
return mScanRecord;
}
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((mDevice == null) ? 0 : mDevice.hashCode());
+ result = prime * result + Arrays.hashCode(mScanRecord);
+ return result;
+ }
@Override
public String toString() {
- return "BluetoothLeDevice [mDevice=" + mDevice + ", mRssi=" + mRssi + ", mScanRecord=" + Arrays.toString(mScanRecord) + ", mRecordStore=" + mRecordStore + ", getBluetoothDeviceBondState()=" + getBluetoothDeviceBondState() + ", getBluetoothDeviceClassName()=" + getBluetoothDeviceClassName() + "]";
+ return "BluetoothLeDevice [mDevice=" + mDevice + ", mRssi=" + mRssi + ", mScanRecord=" + AdRecordUtils.byteArrayToHexString(mScanRecord) + ", mRecordStore=" + mRecordStore + ", getBluetoothDeviceBondState()=" + getBluetoothDeviceBondState() + ", getBluetoothDeviceClassName()=" + getBluetoothDeviceClassName() + "]";
}
+
+
private static String resolveBluetoothClass(int btClass){
switch (btClass){
diff --git a/src/uk/co/alt236/btlescan/util/BluetoothLeScanner.java b/src/uk/co/alt236/btlescan/util/BluetoothLeScanner.java
index c52156c..38d1f79 100644
--- a/src/uk/co/alt236/btlescan/util/BluetoothLeScanner.java
+++ b/src/uk/co/alt236/btlescan/util/BluetoothLeScanner.java
@@ -5,8 +5,6 @@ import android.os.Handler;
import android.util.Log;
public class BluetoothLeScanner {
- private static final long SCAN_PERIOD = 10000;
-
private final Handler mHandler;
private final BluetoothAdapter.LeScanCallback mLeScanCallback;
private final BluetoothUtils mBluetoothUtils;
@@ -18,21 +16,25 @@ public class BluetoothLeScanner {
mBluetoothUtils = bluetoothUtils;
}
-
- public void scanLeDevice(final boolean enable) {
+ public boolean isScanning() {
+ return mScanning;
+ }
+
+ public void scanLeDevice(final int duration, final boolean enable) {
if (enable) {
if(mScanning){return;}
Log.d("TAG", "~ Starting Scan");
// Stops scanning after a pre-defined scan period.
- mHandler.postDelayed(new Runnable() {
- @Override
- public void run() {
- Log.d("TAG", "~ Stopping Scan (timeout)");
- mScanning = false;
- mBluetoothUtils.getBluetoothAdapter().stopLeScan(mLeScanCallback);
- }
- }, SCAN_PERIOD);
-
+ if(duration > 0){
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ Log.d("TAG", "~ Stopping Scan (timeout)");
+ mScanning = false;
+ mBluetoothUtils.getBluetoothAdapter().stopLeScan(mLeScanCallback);
+ }
+ }, duration);
+ }
mScanning = true;
mBluetoothUtils.getBluetoothAdapter().startLeScan(mLeScanCallback);
} else {