This is mostly working now...

This commit is contained in:
Alexandros Schillings
2014-03-10 00:21:29 +00:00
parent ea0d9a6d72
commit 916dab0e7a
10 changed files with 379 additions and 64 deletions
@@ -0,0 +1,11 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="56dp"
android:layout_height="wrap_content"
android:minWidth="56dp" >
<ProgressBar
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center" />
</FrameLayout>
+68
View File
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
android:id="@+id/device_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="24sp" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingRight="5dp"
android:text="MAC:"
android:textSize="12sp" />
<TextView
android:id="@+id/device_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:typeface="monospace" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=" - "
android:textSize="12sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingRight="5dp"
android:text="RSSI:"
android:textSize="12sp" />
<TextView
android:id="@+id/device_rssi"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingRight="5dp"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
+30 -4
View File
@@ -1,9 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android" > <menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item <item
android:id="@+id/action_settings" android:id="@+id/menu_refresh"
android:checkable="false"
android:orderInCategory="1"
android:showAsAction="ifRoom"/>
<item
android:id="@+id/menu_scan"
android:orderInCategory="100" android:orderInCategory="100"
android:showAsAction="never" android:showAsAction="ifRoom|withText"
android:title="@string/action_settings"/> android:title="@string/menu_scan"/>
<item
android:id="@+id/menu_stop"
android:orderInCategory="101"
android:showAsAction="ifRoom|withText"
android:title="@string/menu_stop"/>
</menu> </menu>
+7
View File
@@ -9,5 +9,12 @@
<string name="not_supported">Not supported</string> <string name="not_supported">Not supported</string>
<string name="supported">Supported</string> <string name="supported">Supported</string>
<string name="no_data">No data</string> <string name="no_data">No data</string>
<string name="unknown_device">Unknown Device</string>
<!-- Menu items -->
<string name="menu_connect">Connect</string>
<string name="menu_disconnect">Disconnect</string>
<string name="menu_scan">Scan</string>
<string name="menu_stop">Stop</string>
</resources> </resources>
+72 -40
View File
@@ -2,6 +2,7 @@ package uk.co.alt236.btlescan;
import java.util.Collection; import java.util.Collection;
import uk.co.alt236.btlescan.adapters.LeDeviceListAdapter;
import uk.co.alt236.btlescan.containers.AdRecord; import uk.co.alt236.btlescan.containers.AdRecord;
import uk.co.alt236.btlescan.containers.AdRecordUtils; import uk.co.alt236.btlescan.containers.AdRecordUtils;
import uk.co.alt236.btlescan.containers.BluetoothLeDevice; import uk.co.alt236.btlescan.containers.BluetoothLeDevice;
@@ -13,6 +14,7 @@ import android.bluetooth.BluetoothDevice;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView; import android.widget.TextView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.InjectView; import butterknife.InjectView;
@@ -20,12 +22,11 @@ import butterknife.InjectView;
public class MainActivity extends ListActivity { public class MainActivity extends ListActivity {
@InjectView(R.id.tvBluetoothLe) TextView mTvBluetoothLeStatus; @InjectView(R.id.tvBluetoothLe) TextView mTvBluetoothLeStatus;
@InjectView(R.id.tvBluetoothStatus) TextView mTvBluetoothStatus; @InjectView(R.id.tvBluetoothStatus) TextView mTvBluetoothStatus;
private static final long SCAN_PERIOD = 10000;
private BluetoothUtils mBluetoothUtils; private BluetoothUtils mBluetoothUtils;
private BluetoothLeScanner mScanner; private BluetoothLeScanner mScanner;
//private LeDeviceListAdapter mLeDeviceListAdapter; private LeDeviceListAdapter mLeDeviceListAdapter;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -35,62 +36,93 @@ public class MainActivity extends ListActivity {
mScanner = new BluetoothLeScanner(mLeScanCallback, mBluetoothUtils); mScanner = new BluetoothLeScanner(mLeScanCallback, mBluetoothUtils);
} }
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) { 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);
getMenuInflater().inflate(R.menu.main, menu); if (!mScanner.isScanning()) {
return true; 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() { private BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() {
@Override @Override
public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) { public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
final BluetoothLeDevice deviceLe = new BluetoothLeDevice(device, rssi, scanRecord); final BluetoothLeDevice deviceLe = new BluetoothLeDevice(device, rssi, scanRecord);
Log.d("TAG", "~ New BT Device: " + deviceLe); Log.d("TAG", "~ New BT Device: " + deviceLe);
final Collection<AdRecord> adRecords = deviceLe.getAdRecordStore().getRecordsAsCollection(); final Collection<AdRecord> adRecords = deviceLe.getAdRecordStore().getRecordsAsCollection();
for(final AdRecord record : adRecords){ for(final AdRecord record : adRecords){
Log.d("TAG", "~ Has Record: " + record.getType() + ": '" + record.getHumanReadableType() +"', data: '"+ AdRecordUtils.getRecordDataAsString(record) + "'"); Log.d("TAG", "~ Has Record: " + record.getType() + ": '" + record.getHumanReadableType() +"', data: '"+ AdRecordUtils.getRecordDataAsString(record) + "'");
} }
// runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
// @Override @Override
// public void run() { public void run() {
// mLeDeviceListAdapter.addDevice(device); mLeDeviceListAdapter.addDevice(deviceLe);
// mLeDeviceListAdapter.notifyDataSetChanged(); mLeDeviceListAdapter.notifyDataSetChanged();
// } }
// }); });
} }
}; };
@Override
protected void onPause() {
super.onPause();
mScanner.scanLeDevice(-1, false);
mLeDeviceListAdapter.clear();
}
@Override @Override
public void onResume(){ public void onResume(){
super.onResume(); super.onResume();
final boolean mIsBluetoothOn = mBluetoothUtils.isBluetoothOn(); final boolean mIsBluetoothOn = mBluetoothUtils.isBluetoothOn();
final boolean mIsBluetoothLePresent = mBluetoothUtils.isBluetoothLeSupported(); final boolean mIsBluetoothLePresent = mBluetoothUtils.isBluetoothLeSupported();
if(mIsBluetoothOn){ if(mIsBluetoothOn){
mTvBluetoothStatus.setText(R.string.on); mTvBluetoothStatus.setText(R.string.on);
} else { } else {
mTvBluetoothStatus.setText(R.string.off); mTvBluetoothStatus.setText(R.string.off);
} }
if(mIsBluetoothLePresent){ if(mIsBluetoothLePresent){
mTvBluetoothLeStatus.setText(R.string.supported); mTvBluetoothLeStatus.setText(R.string.supported);
} else { } else {
mTvBluetoothLeStatus.setText(R.string.not_supported); mTvBluetoothLeStatus.setText(R.string.not_supported);
} }
mLeDeviceListAdapter = new LeDeviceListAdapter(this);
setListAdapter(mLeDeviceListAdapter);
mBluetoothUtils.askUserToEnableBluetoothIfNeeded(); mBluetoothUtils.askUserToEnableBluetoothIfNeeded();
if(mIsBluetoothOn && mIsBluetoothLePresent){ if(mIsBluetoothOn && mIsBluetoothLePresent){
mScanner.scanLeDevice(true); mScanner.scanLeDevice(-1, true);
invalidateOptionsMenu();
} }
} }
@@ -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<BluetoothLeDevice> mLeDevices;
private final LayoutInflater mInflator;
public LeDeviceListAdapter(Activity activity) {
super();
mLeDevices = new ArrayList<BluetoothLeDevice>();
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;
}
}
@@ -7,6 +7,26 @@ import java.util.Arrays;
* AdRecord * AdRecord
*/ */
public final class 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 * General FLAGS
* *
@@ -143,7 +163,7 @@ public final class AdRecord {
case TYPE_MANUFACTURER_SPECIFIC_DATA: case TYPE_MANUFACTURER_SPECIFIC_DATA:
return "Manufacturer Specific Data"; return "Manufacturer Specific Data";
case TYPE_LOCAL_NAME_COMPLETE: case TYPE_LOCAL_NAME_COMPLETE:
return "Name"; return "Name (Complete)";
case TYPE_LOCAL_NAME_SHORT: case TYPE_LOCAL_NAME_SHORT:
return "Name (Short)"; return "Name (Short)";
case TYPE_SECURITY_MANAGER_OOB_FLAGS: case TYPE_SECURITY_MANAGER_OOB_FLAGS:
@@ -11,6 +11,25 @@ import android.annotation.SuppressLint;
public class AdRecordUtils { public class AdRecordUtils {
/* Helper functions to parse out common data payloads from an AD structure */ /* 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) { public static String getRecordDataAsString(AdRecord nameRecord) {
if(nameRecord == null){return new String();} if(nameRecord == null){return new String();}
@@ -64,7 +83,7 @@ public class AdRecordUtils {
return Collections.unmodifiableList(records); return Collections.unmodifiableList(records);
} }
@SuppressLint("UseSparseArrays") @SuppressLint("UseSparseArrays")
public static Map<Integer, AdRecord> parseScanRecordAsMap(byte[] scanRecord) { public static Map<Integer, AdRecord> parseScanRecordAsMap(byte[] scanRecord) {
final Map<Integer, AdRecord> records = new HashMap<Integer, AdRecord>(); final Map<Integer, AdRecord> records = new HashMap<Integer, AdRecord>();
@@ -7,7 +7,7 @@ import android.bluetooth.BluetoothDevice;
public class BluetoothLeDevice { public class BluetoothLeDevice {
private final BluetoothDevice mDevice; private final BluetoothDevice mDevice;
private final int mRssi; private transient final int mRssi;
private final byte[] mScanRecord; private final byte[] mScanRecord;
private final AdRecordStore mRecordStore; private final AdRecordStore mRecordStore;
@@ -18,14 +18,37 @@ public class BluetoothLeDevice {
mRecordStore = new AdRecordStore(AdRecordUtils.parseScanRecordAsMap(scanRecord)); 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(){ public AdRecordStore getAdRecordStore(){
return mRecordStore; return mRecordStore;
} }
public String getBluetoothDeviceBondState(){ public String getBluetoothDeviceBondState(){
return resolveBondingState(mDevice.getBondState()); return resolveBondingState(mDevice.getBondState());
} }
public String getBluetoothDeviceClassName(){ public String getBluetoothDeviceClassName(){
return resolveBluetoothClass(mDevice.getBluetoothClass().getDeviceClass()); return resolveBluetoothClass(mDevice.getBluetoothClass().getDeviceClass());
} }
@@ -33,7 +56,11 @@ public class BluetoothLeDevice {
public BluetoothDevice getDevice() { public BluetoothDevice getDevice() {
return mDevice; return mDevice;
} }
public String getName(){
return mDevice.getName();
}
public int getRssi() { public int getRssi() {
return mRssi; return mRssi;
} }
@@ -41,11 +68,22 @@ public class BluetoothLeDevice {
public byte[] getScanRecord() { public byte[] getScanRecord() {
return mScanRecord; 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 @Override
public String toString() { 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){ private static String resolveBluetoothClass(int btClass){
switch (btClass){ switch (btClass){
@@ -5,8 +5,6 @@ import android.os.Handler;
import android.util.Log; import android.util.Log;
public class BluetoothLeScanner { public class BluetoothLeScanner {
private static final long SCAN_PERIOD = 10000;
private final Handler mHandler; private final Handler mHandler;
private final BluetoothAdapter.LeScanCallback mLeScanCallback; private final BluetoothAdapter.LeScanCallback mLeScanCallback;
private final BluetoothUtils mBluetoothUtils; private final BluetoothUtils mBluetoothUtils;
@@ -18,21 +16,25 @@ public class BluetoothLeScanner {
mBluetoothUtils = bluetoothUtils; mBluetoothUtils = bluetoothUtils;
} }
public boolean isScanning() {
public void scanLeDevice(final boolean enable) { return mScanning;
}
public void scanLeDevice(final int duration, final boolean enable) {
if (enable) { if (enable) {
if(mScanning){return;} if(mScanning){return;}
Log.d("TAG", "~ Starting Scan"); Log.d("TAG", "~ Starting Scan");
// Stops scanning after a pre-defined scan period. // Stops scanning after a pre-defined scan period.
mHandler.postDelayed(new Runnable() { if(duration > 0){
@Override mHandler.postDelayed(new Runnable() {
public void run() { @Override
Log.d("TAG", "~ Stopping Scan (timeout)"); public void run() {
mScanning = false; Log.d("TAG", "~ Stopping Scan (timeout)");
mBluetoothUtils.getBluetoothAdapter().stopLeScan(mLeScanCallback); mScanning = false;
} mBluetoothUtils.getBluetoothAdapter().stopLeScan(mLeScanCallback);
}, SCAN_PERIOD); }
}, duration);
}
mScanning = true; mScanning = true;
mBluetoothUtils.getBluetoothAdapter().startLeScan(mLeScanCallback); mBluetoothUtils.getBluetoothAdapter().startLeScan(mLeScanCallback);
} else { } else {