diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 55fafa8..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -bluetooth-le-library \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 9a8b7e5..96cc43e 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,7 +1,6 @@ - diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 198ad64..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 3b31283..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index c1b4dde..a58deef 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,6 +3,64 @@ + + + + + + + + + + + + + Android + + + Android > Lint > Correctness + + + Android > Lint > Performance + + + CorrectnessLintAndroid + + + General + + + LintAndroid + + + + + Android + + + + + @@ -13,26 +71,10 @@ - + - - - - - Android API 18 Platform - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Run_Android_Tests.xml b/.idea/runConfigurations/Run_Android_Tests.xml deleted file mode 100644 index 1f32fb7..0000000 --- a/.idea/runConfigurations/Run_Android_Tests.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Run_JUnit_Tests.xml b/.idea/runConfigurations/Run_JUnit_Tests.xml deleted file mode 100644 index 09ab733..0000000 --- a/.idea/runConfigurations/Run_JUnit_Tests.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Run_Sample_App.xml b/.idea/runConfigurations/Run_Sample_App.xml deleted file mode 100644 index 8873b95..0000000 --- a/.idea/runConfigurations/Run_Sample_App.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1dd..94a25f7 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/Bluetooth-LE-Library---Android.iml b/Bluetooth-LE-Library---Android.iml new file mode 100644 index 0000000..dcc5098 --- /dev/null +++ b/Bluetooth-LE-Library---Android.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bluetooth-le-library.iml b/bluetooth-le-library.iml index cf2c73f..6c7aa80 100644 --- a/bluetooth-le-library.iml +++ b/bluetooth-le-library.iml @@ -8,12 +8,12 @@ - + - + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 327b2ba..1951a2d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,16 +1,16 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. -ext.compileSdkVersion = 22 -ext.buildToolsVersion = "22.0.1" +ext.compileSdkVersion = 24 +ext.buildToolsVersion = "24.0.1" ext.minSdkVersion = 18 -ext.targetSdkVersion = 22 +ext.targetSdkVersion = 24 buildscript { repositories { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.2.3' + classpath 'com.android.tools.build:gradle:2.2.3' } } diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..23d13c5 --- /dev/null +++ b/circle.yml @@ -0,0 +1,7 @@ + test: + override: + - (echo "Running JUnit tests!") + - ./gradlew test -PdisablePreDex + post: + - mkdir -p $CIRCLE_TEST_REPORTS/junit/ + - find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} $CIRCLE_TEST_REPORTS/junit/ \; diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0c71e76..edae9ca 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Apr 10 15:27:10 PDT 2013 +#Wed Aug 24 14:15:35 BST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/library/library.iml b/library/library.iml index 7656b88..c0cbb2b 100644 --- a/library/library.iml +++ b/library/library.iml @@ -12,19 +12,19 @@ - + @@ -33,8 +33,16 @@ + - + + + + + + + + @@ -42,6 +50,7 @@ + @@ -49,6 +58,7 @@ + @@ -56,6 +66,15 @@ + + + + + + + + + @@ -63,32 +82,27 @@ + + - - - - - - + + - - - - + + - + - diff --git a/sample_app/build.gradle b/sample_app/build.gradle index 38e6eb0..5cbefef 100644 --- a/sample_app/build.gradle +++ b/sample_app/build.gradle @@ -13,14 +13,27 @@ repositories { maven { url "https://repo.commonsware.com.s3.amazonaws.com" } + + maven { + url "https://s3.amazonaws.com/repo.commonsware.com" + } + + maven { + url "https://dl.bintray.com/alt236/maven" + } } dependencies { compile 'com.jakewharton:butterknife:7.0.1' - compile 'com.android.support:appcompat-v7:22.2.0' - compile 'com.commonsware.cwac:merge:1.1.0' + compile 'com.android.support:appcompat-v7:24.2.0' + compile 'com.android.support:recyclerview-v7:24.2.0' + compile 'com.anthonycr.grant:permissions:1.0' + compile 'uk.co.alt236:easycursor-android:1.0.0' compile fileTree(include: ['*.jar'], dir: 'libs') compile project(':library') + + testCompile 'junit:junit:4.12' + testCompile 'org.mockito:mockito-all:1.9.5' } android { diff --git a/sample_app/ic_launcher-web.png b/sample_app/ic_launcher-web.png deleted file mode 100644 index 73c663b..0000000 Binary files a/sample_app/ic_launcher-web.png and /dev/null differ diff --git a/sample_app/libs/EasyCursor-0.1.1.jar b/sample_app/libs/EasyCursor-0.1.1.jar deleted file mode 100644 index 2e043b5..0000000 Binary files a/sample_app/libs/EasyCursor-0.1.1.jar and /dev/null differ diff --git a/sample_app/sample_app.iml b/sample_app/sample_app.iml index 756895c..b140503 100644 --- a/sample_app/sample_app.iml +++ b/sample_app/sample_app.iml @@ -12,9 +12,9 @@ - + @@ -32,8 +32,16 @@ + - + + + + + + + + @@ -41,6 +49,7 @@ + @@ -48,6 +57,7 @@ + @@ -55,6 +65,15 @@ + + + + + + + + + @@ -62,41 +81,64 @@ + + + - - - - - - - + + + + + + + + + + + - - - - + + + + + + - - + + + + + - + - - - - - - - + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample_app/src/main/AndroidManifest.xml b/sample_app/src/main/AndroidManifest.xml index 7546b22..7eafc25 100644 --- a/sample_app/src/main/AndroidManifest.xml +++ b/sample_app/src/main/AndroidManifest.xml @@ -1,38 +1,36 @@ - + - - + + + + android:required="false" /> - + - + - - - + + + + android:enabled="true" /> \ No newline at end of file diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/activities/DeviceDetailsActivity.java b/sample_app/src/main/java/uk/co/alt236/btlescan/activities/DeviceDetailsActivity.java deleted file mode 100644 index 1e3e10f..0000000 --- a/sample_app/src/main/java/uk/co/alt236/btlescan/activities/DeviceDetailsActivity.java +++ /dev/null @@ -1,240 +0,0 @@ -package uk.co.alt236.btlescan.activities; - -import android.content.Intent; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v7.app.AppCompatActivity; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.LinearLayout; -import android.widget.ListView; -import android.widget.TextView; - -import com.commonsware.cwac.merge.MergeAdapter; - -import java.util.Collection; -import java.util.Locale; - -import butterknife.Bind; -import butterknife.ButterKnife; -import uk.co.alt236.bluetoothlelib.device.BluetoothLeDevice; -import uk.co.alt236.bluetoothlelib.device.BluetoothService; -import uk.co.alt236.bluetoothlelib.device.adrecord.AdRecord; -import uk.co.alt236.bluetoothlelib.device.beacon.BeaconType; -import uk.co.alt236.bluetoothlelib.device.beacon.BeaconUtils; -import uk.co.alt236.bluetoothlelib.device.beacon.ibeacon.IBeaconManufacturerData; -import uk.co.alt236.bluetoothlelib.resolvers.CompanyIdentifierResolver; -import uk.co.alt236.bluetoothlelib.util.AdRecordUtils; -import uk.co.alt236.bluetoothlelib.util.ByteUtils; -import uk.co.alt236.btlescan.R; -import uk.co.alt236.btlescan.util.TimeFormatter; - -public class DeviceDetailsActivity extends AppCompatActivity { - public static final String EXTRA_DEVICE = "extra_device"; - @Bind(android.R.id.list) - protected ListView mList; - @Nullable - @Bind(android.R.id.empty) - protected View mEmpty; - private BluetoothLeDevice mDevice; - - private void appendAdRecordView(final MergeAdapter adapter, final String title, final AdRecord record) { - final LinearLayout lt = (LinearLayout) getLayoutInflater().inflate(R.layout.list_item_view_adrecord, null); - final TextView tvString = (TextView) lt.findViewById(R.id.data_as_string); - final TextView tvArray = (TextView) lt.findViewById(R.id.data_as_array); - final TextView tvTitle = (TextView) lt.findViewById(R.id.title); - - tvTitle.setText(title); - tvString.setText("'" + AdRecordUtils.getRecordDataAsString(record) + "'"); - tvArray.setText("'" + ByteUtils.byteArrayToHexString(record.getData()) + "'"); - - adapter.addView(lt); - } - - private void appendDeviceInfo(final MergeAdapter adapter, final BluetoothLeDevice device) { - final LinearLayout lt = (LinearLayout) getLayoutInflater().inflate(R.layout.list_item_view_device_info, null); - final TextView tvName = (TextView) lt.findViewById(R.id.deviceName); - final TextView tvAddress = (TextView) lt.findViewById(R.id.deviceAddress); - final TextView tvClass = (TextView) lt.findViewById(R.id.deviceClass); - final TextView tvMajorClass = (TextView) lt.findViewById(R.id.deviceMajorClass); - final TextView tvServices = (TextView) lt.findViewById(R.id.deviceServiceList); - final TextView tvBondingState = (TextView) lt.findViewById(R.id.deviceBondingState); - - tvName.setText(device.getName()); - tvAddress.setText(device.getAddress()); - tvClass.setText(device.getBluetoothDeviceClassName()); - tvMajorClass.setText(device.getBluetoothDeviceMajorClassName()); - tvBondingState.setText(device.getBluetoothDeviceBondState()); - - final String supportedServices; - if(device.getBluetoothDeviceKnownSupportedServices().isEmpty()){ - supportedServices = getString(R.string.no_known_services); - } else { - final StringBuilder sb = new StringBuilder(); - - for(final BluetoothService service : device.getBluetoothDeviceKnownSupportedServices()){ - if(sb.length() > 0){ - sb.append(", "); - } - - sb.append(service); - } - supportedServices = sb.toString(); - } - - tvServices.setText(supportedServices); - - adapter.addView(lt); - } - - private void appendHeader(final MergeAdapter adapter, final String title) { - final LinearLayout lt = (LinearLayout) getLayoutInflater().inflate(R.layout.list_item_view_header, null); - final TextView tvTitle = (TextView) lt.findViewById(R.id.title); - tvTitle.setText(title); - - adapter.addView(lt); - } - - private void appendIBeaconInfo(final MergeAdapter adapter, final IBeaconManufacturerData iBeaconData) { - final LinearLayout lt = (LinearLayout) getLayoutInflater().inflate(R.layout.list_item_view_ibeacon_details, null); - final TextView tvCompanyId = (TextView) lt.findViewById(R.id.companyId); - final TextView tvAdvert = (TextView) lt.findViewById(R.id.advertisement); - final TextView tvUUID = (TextView) lt.findViewById(R.id.uuid); - final TextView tvMajor = (TextView) lt.findViewById(R.id.major); - final TextView tvMinor = (TextView) lt.findViewById(R.id.minor); - final TextView tvTxPower = (TextView) lt.findViewById(R.id.txpower); - - tvCompanyId.setText( - CompanyIdentifierResolver.getCompanyName(iBeaconData.getCompanyIdentifier(), getString(R.string.unknown)) - + " (" + hexEncode(iBeaconData.getCompanyIdentifier()) + ")"); - tvAdvert.setText(iBeaconData.getIBeaconAdvertisement() + " (" + hexEncode(iBeaconData.getIBeaconAdvertisement()) + ")"); - tvUUID.setText(iBeaconData.getUUID()); - tvMajor.setText(iBeaconData.getMajor() + " (" + hexEncode(iBeaconData.getMajor()) + ")"); - tvMinor.setText(iBeaconData.getMinor() + " (" + hexEncode(iBeaconData.getMinor()) + ")"); - tvTxPower.setText(iBeaconData.getCalibratedTxPower() + " (" + hexEncode(iBeaconData.getCalibratedTxPower()) + ")"); - - adapter.addView(lt); - } - - private void appendRssiInfo(final MergeAdapter adapter, final BluetoothLeDevice device) { - final LinearLayout lt = (LinearLayout) getLayoutInflater().inflate(R.layout.list_item_view_rssi_info, null); - final TextView tvFirstTimestamp = (TextView) lt.findViewById(R.id.firstTimestamp); - final TextView tvFirstRssi = (TextView) lt.findViewById(R.id.firstRssi); - final TextView tvLastTimestamp = (TextView) lt.findViewById(R.id.lastTimestamp); - final TextView tvLastRssi = (TextView) lt.findViewById(R.id.lastRssi); - final TextView tvRunningAverageRssi = (TextView) lt.findViewById(R.id.runningAverageRssi); - - tvFirstTimestamp.setText(formatTime(device.getFirstTimestamp())); - tvFirstRssi.setText(formatRssi(device.getFirstRssi())); - tvLastTimestamp.setText(formatTime(device.getTimestamp())); - tvLastRssi.setText(formatRssi(device.getRssi())); - tvRunningAverageRssi.setText(formatRssi(device.getRunningAverageRssi())); - - adapter.addView(lt); - } - - private void appendSimpleText(final MergeAdapter adapter, final byte[] data) { - appendSimpleText(adapter, ByteUtils.byteArrayToHexString(data)); - } - - private void appendSimpleText(final MergeAdapter adapter, final String data) { - final LinearLayout lt = (LinearLayout) getLayoutInflater().inflate(R.layout.list_item_view_textview, null); - final TextView tvData = (TextView) lt.findViewById(R.id.data); - - tvData.setText(data); - - adapter.addView(lt); - } - - - private String formatRssi(final double rssi) { - return getString(R.string.formatter_db, String.valueOf(rssi)); - } - - private String formatRssi(final int rssi) { - return getString(R.string.formatter_db, String.valueOf(rssi)); - } - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_details); - ButterKnife.bind(this); - - mList.setEmptyView(mEmpty); - - mDevice = getIntent().getParcelableExtra(EXTRA_DEVICE); - - pupulateDetails(mDevice); - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - getMenuInflater().inflate(R.menu.details, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_connect: - - final Intent intent = new Intent(this, DeviceControlActivity.class); - intent.putExtra(DeviceControlActivity.EXTRA_DEVICE, mDevice); - - startActivity(intent); - - break; - } - return true; - } - - private void pupulateDetails(final BluetoothLeDevice device) { - final MergeAdapter adapter = new MergeAdapter(); - - if (device == null) { - appendHeader(adapter, getString(R.string.header_device_info)); - appendSimpleText(adapter, getString(R.string.invalid_device_data)); - } else { - appendHeader(adapter, getString(R.string.header_device_info)); - appendDeviceInfo(adapter, device); - - appendHeader(adapter, getString(R.string.header_rssi_info)); - appendRssiInfo(adapter, device); - - appendHeader(adapter, getString(R.string.header_scan_record)); - appendSimpleText(adapter, device.getScanRecord()); - - final Collection adRecords = device.getAdRecordStore().getRecordsAsCollection(); - if (adRecords.size() > 0) { - appendHeader(adapter, getString(R.string.header_raw_ad_records)); - - for (final AdRecord record : adRecords) { - - appendAdRecordView( - adapter, - "#" + record.getType() + " " + record.getHumanReadableType(), - record); - } - } - - final boolean isIBeacon = BeaconUtils.getBeaconType(device) == BeaconType.IBEACON; - if (isIBeacon) { - final IBeaconManufacturerData iBeaconData = new IBeaconManufacturerData(device); - appendHeader(adapter, getString(R.string.header_ibeacon_data)); - appendIBeaconInfo(adapter, iBeaconData); - } - - } - mList.setAdapter(adapter); - } - - private static String formatTime(final long time) { - return TimeFormatter.getIsoDateTime(time); - } - - private static String hexEncode(final int integer) { - return "0x" + Integer.toHexString(integer).toUpperCase(Locale.US); - } -} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/adapters/LeDeviceListAdapter.java b/sample_app/src/main/java/uk/co/alt236/btlescan/adapters/LeDeviceListAdapter.java deleted file mode 100644 index 97cbdbc..0000000 --- a/sample_app/src/main/java/uk/co/alt236/btlescan/adapters/LeDeviceListAdapter.java +++ /dev/null @@ -1,126 +0,0 @@ -package uk.co.alt236.btlescan.adapters; - -import android.app.Activity; -import android.support.v4.widget.SimpleCursorAdapter; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import uk.co.alt236.bluetoothlelib.device.BluetoothLeDevice; -import uk.co.alt236.bluetoothlelib.device.beacon.BeaconType; -import uk.co.alt236.bluetoothlelib.device.beacon.BeaconUtils; -import uk.co.alt236.bluetoothlelib.device.beacon.ibeacon.IBeaconDevice; -import uk.co.alt236.btlescan.R; -import uk.co.alt236.btlescan.util.Constants; -import uk.co.alt236.easycursor.objectcursor.EasyObjectCursor; - -// Adapter for holding devices found through scanning. -public class LeDeviceListAdapter extends SimpleCursorAdapter { - private final LayoutInflater mInflator; - private final Activity mActivity; - - public LeDeviceListAdapter(final Activity activity, final EasyObjectCursor cursor) { - super(activity, R.layout.list_item_device, cursor, new String[0], new int[0], 0); - mInflator = activity.getLayoutInflater(); - mActivity = activity; - } - - @SuppressWarnings("unchecked") - @Override - public EasyObjectCursor getCursor() { - return ((EasyObjectCursor) super.getCursor()); - } - - @Override - public BluetoothLeDevice getItem(final int i) { - return getCursor().getItem(i); - } - - @Override - public long getItemId(final int i) { - return i; - } - - @Override - public View getView(final int i, View view, final ViewGroup viewGroup) { - final 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); - viewHolder.deviceIcon = (ImageView) view.findViewById(R.id.device_icon); - viewHolder.deviceLastUpdated = (TextView) view.findViewById(R.id.device_last_update); - viewHolder.ibeaconMajor = (TextView) view.findViewById(R.id.ibeacon_major); - viewHolder.ibeaconMinor = (TextView) view.findViewById(R.id.ibeacon_minor); - viewHolder.ibeaconDistance = (TextView) view.findViewById(R.id.ibeacon_distance); - viewHolder.ibeaconUUID = (TextView) view.findViewById(R.id.ibeacon_uuid); - viewHolder.ibeaconTxPower = (TextView) view.findViewById(R.id.ibeacon_tx_power); - viewHolder.ibeaconSection = view.findViewById(R.id.ibeacon_section); - viewHolder.ibeaconDistanceDescriptor = (TextView) view.findViewById(R.id.ibeacon_distance_descriptor); - view.setTag(viewHolder); - } else { - viewHolder = (ViewHolder) view.getTag(); - } - - final BluetoothLeDevice device = getCursor().getItem(i); - final String deviceName = device.getName(); - final double rssi = device.getRssi(); - - if (deviceName != null && deviceName.length() > 0) { - viewHolder.deviceName.setText(deviceName); - } else { - viewHolder.deviceName.setText(R.string.unknown_device); - } - - if (BeaconUtils.getBeaconType(device) == BeaconType.IBEACON) { - final IBeaconDevice iBeacon = new IBeaconDevice(device); - final String accuracy = Constants.DOUBLE_TWO_DIGIT_ACCURACY.format(iBeacon.getAccuracy()); - - viewHolder.deviceIcon.setImageResource(R.drawable.ic_device_ibeacon); - viewHolder.ibeaconSection.setVisibility(View.VISIBLE); - viewHolder.ibeaconMajor.setText(String.valueOf(iBeacon.getMajor())); - viewHolder.ibeaconMinor.setText(String.valueOf(iBeacon.getMinor())); - viewHolder.ibeaconTxPower.setText(String.valueOf(iBeacon.getCalibratedTxPower())); - viewHolder.ibeaconUUID.setText(iBeacon.getUUID()); - viewHolder.ibeaconDistance.setText( - mActivity.getString(R.string.formatter_meters, accuracy)); - viewHolder.ibeaconDistanceDescriptor.setText(iBeacon.getDistanceDescriptor().toString()); - } else { - viewHolder.deviceIcon.setImageResource(R.drawable.ic_bluetooth); - viewHolder.ibeaconSection.setVisibility(View.GONE); - } - - final String rssiString = - mActivity.getString(R.string.formatter_db, String.valueOf(rssi)); - final String runningAverageRssiString = - mActivity.getString(R.string.formatter_db, String.valueOf(device.getRunningAverageRssi())); - - viewHolder.deviceLastUpdated.setText( - android.text.format.DateFormat.format( - Constants.TIME_FORMAT, new java.util.Date(device.getTimestamp()))); - viewHolder.deviceAddress.setText(device.getAddress()); - viewHolder.deviceRssi.setText(rssiString + " / " + runningAverageRssiString); - return view; - } - - static class ViewHolder { - TextView deviceName; - TextView deviceAddress; - TextView deviceRssi; - TextView ibeaconUUID; - TextView ibeaconMajor; - TextView ibeaconMinor; - TextView ibeaconTxPower; - TextView ibeaconDistance; - TextView ibeaconDistanceDescriptor; - TextView deviceLastUpdated; - View ibeaconSection; - ImageView deviceIcon; - } - -} \ No newline at end of file diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/containers/BluetoothLeDeviceStore.java b/sample_app/src/main/java/uk/co/alt236/btlescan/containers/BluetoothLeDeviceStore.java index 8b99033..c591de7 100644 --- a/sample_app/src/main/java/uk/co/alt236/btlescan/containers/BluetoothLeDeviceStore.java +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/containers/BluetoothLeDeviceStore.java @@ -1,39 +1,26 @@ package uk.co.alt236.btlescan.containers; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; +import android.support.annotation.NonNull; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import uk.co.alt236.bluetoothlelib.device.BluetoothLeDevice; -import uk.co.alt236.bluetoothlelib.device.beacon.BeaconType; -import uk.co.alt236.bluetoothlelib.device.beacon.BeaconUtils; -import uk.co.alt236.bluetoothlelib.device.beacon.ibeacon.IBeaconDevice; -import uk.co.alt236.bluetoothlelib.util.ByteUtils; -import uk.co.alt236.btlescan.R; -import uk.co.alt236.btlescan.util.CsvWriterHelper; -import uk.co.alt236.btlescan.util.TimeFormatter; import uk.co.alt236.easycursor.objectcursor.EasyObjectCursor; public class BluetoothLeDeviceStore { + private static final BluetoothLeDeviceComparator DEFAULT_COMPARATOR = new BluetoothLeDeviceComparator(); private final Map mDeviceMap; - public BluetoothLeDeviceStore() { mDeviceMap = new HashMap<>(); } - public void addDevice(final BluetoothLeDevice device) { + public void addDevice(@NonNull final BluetoothLeDevice device) { if (mDeviceMap.containsKey(device.getAddress())) { mDeviceMap.get(device.getAddress()).updateRssiReading(device.getTimestamp(), device.getRssi()); } else { @@ -45,137 +32,42 @@ public class BluetoothLeDeviceStore { mDeviceMap.clear(); } + public int getSize() { + return mDeviceMap.size(); + } + + @NonNull public EasyObjectCursor getDeviceCursor() { + return getDeviceCursor(DEFAULT_COMPARATOR); + } + + @NonNull + public EasyObjectCursor getDeviceCursor(@NonNull Comparator comparator) { return new EasyObjectCursor<>( BluetoothLeDevice.class, - getDeviceList(), + getDeviceList(comparator), "address"); } + @NonNull public List getDeviceList() { + return getDeviceList(DEFAULT_COMPARATOR); + } + + @NonNull + public List getDeviceList(@NonNull Comparator comparator) { final List methodResult = new ArrayList<>(mDeviceMap.values()); - Collections.sort(methodResult, new Comparator() { - - @Override - public int compare(final BluetoothLeDevice arg0, final BluetoothLeDevice arg1) { - return arg0.getAddress().compareToIgnoreCase(arg1.getAddress()); - } - }); + Collections.sort(methodResult, comparator); return methodResult; } - private String getListAsCsv() { - final List list = getDeviceList(); - final StringBuilder sb = new StringBuilder(); - sb.append(CsvWriterHelper.addStuff("mac")); - sb.append(CsvWriterHelper.addStuff("name")); - sb.append(CsvWriterHelper.addStuff("firstTimestamp")); - sb.append(CsvWriterHelper.addStuff("firstRssi")); - sb.append(CsvWriterHelper.addStuff("currentTimestamp")); - sb.append(CsvWriterHelper.addStuff("currentRssi")); - sb.append(CsvWriterHelper.addStuff("adRecord")); - sb.append(CsvWriterHelper.addStuff("iBeacon")); - sb.append(CsvWriterHelper.addStuff("uuid")); - sb.append(CsvWriterHelper.addStuff("major")); - sb.append(CsvWriterHelper.addStuff("minor")); - sb.append(CsvWriterHelper.addStuff("txPower")); - sb.append(CsvWriterHelper.addStuff("distance")); - sb.append(CsvWriterHelper.addStuff("accuracy")); - sb.append('\n'); + private static class BluetoothLeDeviceComparator implements Comparator { - for (final BluetoothLeDevice device : list) { - sb.append(CsvWriterHelper.addStuff(device.getAddress())); - sb.append(CsvWriterHelper.addStuff(device.getName())); - sb.append(CsvWriterHelper.addStuff(TimeFormatter.getIsoDateTime(device.getFirstTimestamp()))); - sb.append(CsvWriterHelper.addStuff(device.getFirstRssi())); - sb.append(CsvWriterHelper.addStuff(TimeFormatter.getIsoDateTime(device.getTimestamp()))); - sb.append(CsvWriterHelper.addStuff(device.getRssi())); - sb.append(CsvWriterHelper.addStuff(ByteUtils.byteArrayToHexString(device.getScanRecord()))); - final boolean isIBeacon = BeaconUtils.getBeaconType(device) == BeaconType.IBEACON; - final String uuid; - final String minor; - final String major; - final String txPower; - final String distance; - final String accuracy; - - if (isIBeacon) { - final IBeaconDevice beacon = new IBeaconDevice(device); - uuid = String.valueOf(beacon.getUUID()); - minor = String.valueOf(beacon.getMinor()); - major = String.valueOf(beacon.getMajor()); - txPower = String.valueOf(beacon.getCalibratedTxPower()); - distance = beacon.getDistanceDescriptor().toString().toLowerCase(Locale.US); - accuracy = String.valueOf(beacon.getAccuracy()); - } else { - uuid = ""; - minor = ""; - major = ""; - txPower = ""; - distance = ""; - accuracy = ""; - } - - sb.append(CsvWriterHelper.addStuff(isIBeacon)); - sb.append(CsvWriterHelper.addStuff(uuid)); - sb.append(CsvWriterHelper.addStuff(minor)); - sb.append(CsvWriterHelper.addStuff(major)); - sb.append(CsvWriterHelper.addStuff(txPower)); - sb.append(CsvWriterHelper.addStuff(distance)); - sb.append(CsvWriterHelper.addStuff(accuracy)); - - sb.append('\n'); + @Override + public int compare(final BluetoothLeDevice arg0, final BluetoothLeDevice arg1) { + return arg0.getAddress().compareTo(arg1.getAddress()); } - - return sb.toString(); - } - - public void shareDataAsEmail(final Context context) { - final long timeInMillis = System.currentTimeMillis(); - - final String to = null; - final String subject = context.getString( - R.string.exporter_email_device_list_subject, - TimeFormatter.getIsoDateTime(timeInMillis)); - - final String message = context.getString(R.string.exporter_email_device_list_body); - - final Intent i = new Intent(Intent.ACTION_SEND); - i.setType("plain/text"); - try { - final File outputDir = context.getCacheDir(); - final File outputFile = File.createTempFile("bluetooth_le_" + timeInMillis, ".csv", outputDir); - outputFile.setReadable(true, false); - generateFile(outputFile, getListAsCsv()); - i.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(outputFile)); - i.putExtra(Intent.EXTRA_EMAIL, new String[]{to}); - i.putExtra(Intent.EXTRA_SUBJECT, subject); - i.putExtra(Intent.EXTRA_TEXT, message); - context.startActivity(Intent.createChooser(i, context.getString(R.string.exporter_email_device_list_picker_text))); - - } catch (final IOException e) { - e.printStackTrace(); - } - } - - private static FileWriter generateFile(final File file, final String contents) { - FileWriter writer = null; - try { - writer = new FileWriter(file); - writer.append(contents); - writer.flush(); - - } catch (final IOException e) { - e.printStackTrace(); - } finally { - try { - writer.close(); - } catch (final IOException e) { - e.printStackTrace(); - } - } - return writer; } } diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/services/BluetoothLeService.java b/sample_app/src/main/java/uk/co/alt236/btlescan/services/BluetoothLeService.java index bc33610..2320a13 100644 --- a/sample_app/src/main/java/uk/co/alt236/btlescan/services/BluetoothLeService.java +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/services/BluetoothLeService.java @@ -38,22 +38,21 @@ import java.util.List; * given Bluetooth LE device. */ public class BluetoothLeService extends Service { - public final static String ACTION_GATT_CONNECTED = "com.example.bluetooth.le.ACTION_GATT_CONNECTED"; - public final static String ACTION_GATT_DISCONNECTED = "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED"; - public final static String ACTION_GATT_SERVICES_DISCOVERED = "com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED"; - public final static String ACTION_DATA_AVAILABLE = "com.example.bluetooth.le.ACTION_DATA_AVAILABLE"; - public final static String EXTRA_DATA_RAW = "com.example.bluetooth.le.EXTRA_DATA_RAW"; - public final static String EXTRA_UUID_CHAR = "com.example.bluetooth.le.EXTRA_UUID_CHAR"; + public final static String ACTION_GATT_CONNECTED = BluetoothLeService.class.getName() + ".ACTION_GATT_CONNECTED"; + public final static String ACTION_GATT_CONNECTING = BluetoothLeService.class.getName() + ".ACTION_GATT_CONNECTING"; + public final static String ACTION_GATT_DISCONNECTED = BluetoothLeService.class.getName() + ".ACTION_GATT_DISCONNECTED"; + public final static String ACTION_GATT_SERVICES_DISCOVERED = BluetoothLeService.class.getName() + ".ACTION_GATT_SERVICES_DISCOVERED"; + public final static String ACTION_DATA_AVAILABLE = BluetoothLeService.class.getName() + ".ACTION_DATA_AVAILABLE"; + public final static String EXTRA_DATA_RAW = BluetoothLeService.class.getName() + ".EXTRA_DATA_RAW"; + public final static String EXTRA_UUID_CHAR = BluetoothLeService.class.getName() + ".EXTRA_UUID_CHAR"; private final static String TAG = BluetoothLeService.class.getSimpleName(); - private static final int STATE_DISCONNECTED = 0; - private static final int STATE_CONNECTING = 1; - private static final int STATE_CONNECTED = 2; private final IBinder mBinder = new LocalBinder(); private BluetoothManager mBluetoothManager; private BluetoothAdapter mBluetoothAdapter; private String mBluetoothDeviceAddress; private BluetoothGatt mBluetoothGatt; - private int mConnectionState = STATE_DISCONNECTED; + private State mConnectionState = State.DISCONNECTED; + // Implements callback methods for GATT events that the app cares about. For example, // connection change and services discovered. private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { @@ -63,28 +62,35 @@ public class BluetoothLeService extends Service { } @Override - public void onCharacteristicRead(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) { + public void onCharacteristicRead(final BluetoothGatt gatt, + final BluetoothGattCharacteristic characteristic, + final int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic); } } @Override - public void onConnectionStateChange(final BluetoothGatt gatt, final int status, final int newState) { - final String intentAction; + public void onConnectionStateChange(final BluetoothGatt gatt, + final int status, + final int newState) { + + Log.d(TAG, "onConnectionStateChange: status=" + status + ", newState=" + newState); + if (newState == BluetoothProfile.STATE_CONNECTED) { - intentAction = ACTION_GATT_CONNECTED; - mConnectionState = STATE_CONNECTED; - broadcastUpdate(intentAction); + setConnectionState(State.CONNECTED, true); Log.i(TAG, "Connected to GATT server."); // Attempts to discover services after successful connection. Log.i(TAG, "Attempting to start service discovery:" + mBluetoothGatt.discoverServices()); } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { - intentAction = ACTION_GATT_DISCONNECTED; - mConnectionState = STATE_DISCONNECTED; + // Make sure we tidy up. On certain devices reusing a Gatt after a disconnection + // can cause problems. + disconnect(); + + setConnectionState(State.DISCONNECTED, true); Log.i(TAG, "Disconnected from GATT server."); - broadcastUpdate(intentAction); } } @@ -138,37 +144,73 @@ public class BluetoothLeService extends Service { * callback. */ public boolean connect(final String address) { + + final boolean retVal; if (mBluetoothAdapter == null || address == null) { Log.w(TAG, "BluetoothAdapter not initialized or unspecified address."); - return false; - } + retVal = false; - // Previously connected device. Try to reconnect. - if (mBluetoothDeviceAddress != null + // Previously connected device. Try to reconnect. + } else if (mBluetoothDeviceAddress != null && address.equals(mBluetoothDeviceAddress) && mBluetoothGatt != null) { Log.d(TAG, "Trying to use an existing mBluetoothGatt for connection."); if (mBluetoothGatt.connect()) { - mConnectionState = STATE_CONNECTING; - return true; + Log.d(TAG, "Connection attempt OK."); + setConnectionState(State.CONNECTING, true); + retVal = true; } else { - return false; + Log.w(TAG, "Connection attempt failed."); + setConnectionState(State.DISCONNECTED, true); + retVal = false; + } + } else { + + final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); + if (device == null) { + Log.w(TAG, "Device not found. Unable to connect."); + retVal = false; + } else { + // We want to directly connect to the device, so we are setting the autoConnect + // parameter to false. + + Log.d(TAG, "Trying to create a new connection."); + mBluetoothGatt = device.connectGatt(this, false, mGattCallback); + mBluetoothDeviceAddress = address; + setConnectionState(State.CONNECTING, true); + retVal = true; } } - final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); - if (device == null) { - Log.w(TAG, "Device not found. Unable to connect."); - return false; + return retVal; + } + + private synchronized void setConnectionState(final State newState, final boolean broadCast) { + Log.i(TAG, "Setting internal state to " + newState); + mConnectionState = newState; + + final String broadcastAction; + + switch (newState) { + case CONNECTED: + broadcastAction = ACTION_GATT_CONNECTED; + break; + case CONNECTING: + broadcastAction = ACTION_GATT_CONNECTING; + break; + case DISCONNECTED: + broadcastAction = ACTION_GATT_DISCONNECTED; + break; + default: + throw new IllegalArgumentException("Unknown state: " + newState); } - // We want to directly connect to the device, so we are setting the autoConnect - // parameter to false. - mBluetoothGatt = device.connectGatt(this, false, mGattCallback); - Log.d(TAG, "Trying to create a new connection."); - mBluetoothDeviceAddress = address; - mConnectionState = STATE_CONNECTING; - return true; + + if (broadCast) { + Log.i(TAG, "Broadcasting " + broadcastAction); + broadcastUpdate(broadcastAction); + } + } /** @@ -183,6 +225,9 @@ public class BluetoothLeService extends Service { return; } mBluetoothGatt.disconnect(); + + // Reusing a Gatt after disconnecting can cause problems + mBluetoothGatt = null; } /** @@ -270,4 +315,10 @@ public class BluetoothLeService extends Service { return BluetoothLeService.this; } } + + private enum State { + DISCONNECTED, + CONNECTING, + CONNECTED + } } \ No newline at end of file diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/common/Navigation.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/common/Navigation.java new file mode 100644 index 0000000..95846d6 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/common/Navigation.java @@ -0,0 +1,50 @@ +package uk.co.alt236.btlescan.ui.common; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.support.v4.app.ActivityCompat; + +import uk.co.alt236.bluetoothlelib.device.BluetoothLeDevice; +import uk.co.alt236.btlescan.R; +import uk.co.alt236.btlescan.ui.control.DeviceControlActivity; +import uk.co.alt236.btlescan.ui.details.DeviceDetailsActivity; + +public class Navigation { + + private final Activity mActivity; + + public Navigation(final Activity activity) { + mActivity = activity; + } + + public void openDetailsActivity(final BluetoothLeDevice device) { + final Intent intent = DeviceDetailsActivity.createIntent(mActivity, device); + + startActivity(intent); + } + + public void startControlActivity(final BluetoothLeDevice device) { + final Intent intent = DeviceControlActivity.createIntent(mActivity, device); + + startActivity(intent); + } + + public void shareFileViaEmail(final Uri fileUri, final String[] recipient, final String subject, final String message) { + final Intent intent = new Intent(Intent.ACTION_SEND); + + intent.setType("plain/text"); + intent.putExtra(Intent.EXTRA_STREAM, fileUri); + intent.putExtra(Intent.EXTRA_EMAIL, recipient); + intent.putExtra(Intent.EXTRA_SUBJECT, subject); + intent.putExtra(Intent.EXTRA_TEXT, message); + + startActivity(Intent.createChooser(intent, + mActivity.getString(R.string.exporter_email_device_list_picker_text))); + } + + + private void startActivity(final Intent intent) { + ActivityCompat.startActivity(mActivity, intent, null); + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/common/recyclerview/BaseRecyclerViewAdapter.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/common/recyclerview/BaseRecyclerViewAdapter.java new file mode 100644 index 0000000..4181345 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/common/recyclerview/BaseRecyclerViewAdapter.java @@ -0,0 +1,69 @@ +package uk.co.alt236.btlescan.ui.common.recyclerview; + +import android.support.v7.widget.RecyclerView; +import android.view.ViewGroup; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public abstract class BaseRecyclerViewAdapter extends RecyclerView.Adapter> { + + private final List mItemList; + private final RecyclerViewBinderCore mCore; + + public BaseRecyclerViewAdapter(final RecyclerViewBinderCore core, + final List items) { + mItemList = new ArrayList<>(); + mCore = core; + mItemList.addAll(items); + } + + public BaseRecyclerViewAdapter(final RecyclerViewBinderCore core) { + this(core, new ArrayList()); + } + + @Override + public BaseViewHolder onCreateViewHolder(final ViewGroup parent, + final int viewType) { + return mCore.create(parent, viewType); + } + + @Override + public void onBindViewHolder(final BaseViewHolder holder, + final int position) { + + final int viewType = getItemViewType(position); + final BaseViewBinder binder = mCore.getBinder(viewType); + bind(binder, holder, getItem(position)); + } + + @Override + public int getItemCount() { + return mItemList.size(); + } + + @Override + public int getItemViewType(int position) { + return mCore.getViewType(getItem(position)); + } + + public RecyclerViewItem getItem(final int position) { + return mItemList.get(position); + } + + public void setData(Collection data) { + + mItemList.clear(); + mItemList.addAll(data); + notifyDataSetChanged(); + } + + private static void bind(final BaseViewBinder binder, + final BaseViewHolder holder, + final RecyclerViewItem item) { + + //noinspection unchecked + binder.bind((BaseViewHolder) holder, (T) item); + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/common/recyclerview/BaseViewBinder.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/common/recyclerview/BaseViewBinder.java new file mode 100644 index 0000000..ef4b23c --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/common/recyclerview/BaseViewBinder.java @@ -0,0 +1,20 @@ +package uk.co.alt236.btlescan.ui.common.recyclerview; + +import android.content.Context; + +public abstract class BaseViewBinder { + + private final Context mContext; + + public BaseViewBinder(final Context context) { + mContext = context; + } + + public abstract void bind(BaseViewHolder holder, T item); + + public abstract boolean canBind(RecyclerViewItem item); + + protected Context getContext() { + return mContext; + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/common/recyclerview/BaseViewHolder.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/common/recyclerview/BaseViewHolder.java new file mode 100644 index 0000000..b8617c6 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/common/recyclerview/BaseViewHolder.java @@ -0,0 +1,18 @@ +package uk.co.alt236.btlescan.ui.common.recyclerview; + +import android.support.v7.widget.RecyclerView; +import android.view.View; + +public abstract class BaseViewHolder extends RecyclerView.ViewHolder { + + private final View mItemView; + + public BaseViewHolder(final View itemView) { + super(itemView); + mItemView = itemView; + } + + public View getView() { + return mItemView; + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/common/recyclerview/RecyclerViewBinderCore.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/common/recyclerview/RecyclerViewBinderCore.java new file mode 100644 index 0000000..acadb5c --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/common/recyclerview/RecyclerViewBinderCore.java @@ -0,0 +1,99 @@ +package uk.co.alt236.btlescan.ui.common.recyclerview; + +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; + +public class RecyclerViewBinderCore { + public static final int INVALID_VIEWTYPE = -1; + + private static final String TAG = RecyclerViewBinderCore.class.getSimpleName(); + private final List>> mViewHolderClasses; + private final List> mViewBinders; + private final List mLayoutIds; + + public RecyclerViewBinderCore() { + mViewBinders = new ArrayList<>(); + mViewHolderClasses = new ArrayList<>(); + mLayoutIds = new ArrayList<>(); + } + + public void clear() { + mViewBinders.clear(); + mViewHolderClasses.clear(); + mLayoutIds.clear(); + } + + public void add( + final BaseViewBinder binder, + final Class> viewHolder, + final int layoutId) { + + mViewBinders.add(binder); + mViewHolderClasses.add(viewHolder); + mLayoutIds.add(layoutId); + } + + public BaseViewHolder create(ViewGroup parent, final int viewType) { + if (viewType == INVALID_VIEWTYPE) { + throw new IllegalArgumentException("Invalid viewType: " + viewType); + } + + final Class clazz = mViewHolderClasses.get(viewType); + final int layoutId = mLayoutIds.get(viewType); + final View itemView = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false); + + return (BaseViewHolder) instantiate(clazz, itemView); + } + + public int getViewType(final T item) { + int result = INVALID_VIEWTYPE; + int count = 0; + + for (final BaseViewBinder binder : mViewBinders) { + + if (binder.canBind(item)) { + result = count; + break; + } + + count++; + } + + if (result == INVALID_VIEWTYPE) { + Log.w(TAG, "Could not get viewType for " + item); + } + + return result; + } + + public BaseViewBinder getBinder(int viewType) { + if (viewType == INVALID_VIEWTYPE) { + throw new IllegalArgumentException("Invalid viewType: " + viewType); + } + + return mViewBinders.get(viewType); + } + + @SuppressWarnings("TryWithIdenticalCatches") + private static Object instantiate( + final Class clazz, View parentView) { + try { + final Constructor constructor + = clazz.getDeclaredConstructors()[0]; + return constructor.newInstance(parentView); + } catch (InstantiationException e) { + throw new IllegalStateException(e); + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } catch (InvocationTargetException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/common/recyclerview/RecyclerViewItem.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/common/recyclerview/RecyclerViewItem.java new file mode 100644 index 0000000..32b5df9 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/common/recyclerview/RecyclerViewItem.java @@ -0,0 +1,4 @@ +package uk.co.alt236.btlescan.ui.common.recyclerview; + +public interface RecyclerViewItem { +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/activities/DeviceControlActivity.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/control/DeviceControlActivity.java similarity index 58% rename from sample_app/src/main/java/uk/co/alt236/btlescan/activities/DeviceControlActivity.java rename to sample_app/src/main/java/uk/co/alt236/btlescan/ui/control/DeviceControlActivity.java index 5e3753b..bce6683 100644 --- a/sample_app/src/main/java/uk/co/alt236/btlescan/activities/DeviceControlActivity.java +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/control/DeviceControlActivity.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package uk.co.alt236.btlescan.activities; +package uk.co.alt236.btlescan.ui.control; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattService; @@ -26,6 +26,7 @@ import android.content.IntentFilter; import android.content.ServiceConnection; import android.os.Bundle; import android.os.IBinder; +import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.Menu; @@ -35,10 +36,7 @@ import android.widget.ExpandableListView; import android.widget.SimpleExpandableListAdapter; import android.widget.TextView; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import butterknife.Bind; import butterknife.ButterKnife; @@ -55,10 +53,8 @@ import uk.co.alt236.btlescan.services.BluetoothLeService; * Bluetooth LE API. */ public class DeviceControlActivity extends AppCompatActivity { - public static final String EXTRA_DEVICE = "extra_device"; + private static final String EXTRA_DEVICE = DeviceControlActivity.class.getName() + ".EXTRA_DEVICE"; private final static String TAG = DeviceControlActivity.class.getSimpleName(); - private static final String LIST_NAME = "NAME"; - private static final String LIST_UUID = "UUID"; @Bind(R.id.gatt_services_list) protected ExpandableListView mGattServicesList; @Bind(R.id.connection_state) @@ -71,9 +67,10 @@ public class DeviceControlActivity extends AppCompatActivity { protected TextView mDataAsString; @Bind(R.id.data_as_array) protected TextView mDataAsArray; + private Exporter mExporter; private BluetoothGattCharacteristic mNotifyCharacteristic; private BluetoothLeService mBluetoothLeService; - private List> mGattCharacteristics = new ArrayList<>(); + // If a given GATT characteristic is selected, check for supported features. This sample // demonstrates 'Read' and 'Notify' features. See // http://d.android.com/reference/android/bluetooth/BluetoothGatt.html for the complete @@ -81,28 +78,30 @@ public class DeviceControlActivity extends AppCompatActivity { private final ExpandableListView.OnChildClickListener servicesListClickListner = new ExpandableListView.OnChildClickListener() { @Override public boolean onChildClick(final ExpandableListView parent, final View v, final int groupPosition, final int childPosition, final long id) { - if (mGattCharacteristics != null) { - final BluetoothGattCharacteristic characteristic = mGattCharacteristics.get(groupPosition).get(childPosition); - final int charaProp = characteristic.getProperties(); - if ((charaProp | BluetoothGattCharacteristic.PROPERTY_READ) > 0) { - // If there is an active notification on a characteristic, clear - // it first so it doesn't update the data field on the user interface. - if (mNotifyCharacteristic != null) { - mBluetoothLeService.setCharacteristicNotification(mNotifyCharacteristic, false); - mNotifyCharacteristic = null; - } - mBluetoothLeService.readCharacteristic(characteristic); + final GattDataAdapterFactory.GattDataAdapter adapter = + (GattDataAdapterFactory.GattDataAdapter) parent.getExpandableListAdapter(); + + final BluetoothGattCharacteristic characteristic = + adapter.getBluetoothGattCharacteristic(groupPosition, childPosition); + + final int charaProp = characteristic.getProperties(); + if ((charaProp | BluetoothGattCharacteristic.PROPERTY_READ) > 0) { + // If there is an active notification on a characteristic, clear + // it first so it doesn't update the data field on the user interface. + if (mNotifyCharacteristic != null) { + mBluetoothLeService.setCharacteristicNotification(mNotifyCharacteristic, false); + mNotifyCharacteristic = null; } - if ((charaProp | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) { - mNotifyCharacteristic = characteristic; - mBluetoothLeService.setCharacteristicNotification(characteristic, true); - } - return true; + mBluetoothLeService.readCharacteristic(characteristic); } - return false; + if ((charaProp | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) { + mNotifyCharacteristic = characteristic; + mBluetoothLeService.setCharacteristicNotification(characteristic, true); + } + return true; } }; - private String mDeviceAddress; + // Code to manage Service lifecycle. private final ServiceConnection mServiceConnection = new ServiceConnection() { @Override @@ -113,7 +112,7 @@ public class DeviceControlActivity extends AppCompatActivity { finish(); } // Automatically connects to the device upon successful start-up initialization. - mBluetoothLeService.connect(mDeviceAddress); + mBluetoothLeService.connect(mDevice.getAddress()); } @Override @@ -121,8 +120,9 @@ public class DeviceControlActivity extends AppCompatActivity { mBluetoothLeService = null; } }; - private String mDeviceName; - private boolean mConnected = false; + private BluetoothLeDevice mDevice; + private State mCurrentState = State.DISCONNECTED; + private String mExportString; // Handles various events fired by the Service. // ACTION_GATT_CONNECTED: connected to a GATT server. @@ -135,14 +135,16 @@ public class DeviceControlActivity extends AppCompatActivity { public void onReceive(final Context context, final Intent intent) { final String action = intent.getAction(); if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) { - mConnected = true; - updateConnectionState(R.string.connected); + updateConnectionState(State.CONNECTED); invalidateOptionsMenu(); } else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) { - mConnected = false; - updateConnectionState(R.string.disconnected); - invalidateOptionsMenu(); clearUI(); + updateConnectionState(State.DISCONNECTED); + invalidateOptionsMenu(); + } else if (BluetoothLeService.ACTION_GATT_CONNECTING.equals(action)) { + clearUI(); + updateConnectionState(State.CONNECTING); + invalidateOptionsMenu(); } else if (BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED.equals(action)) { // Show all the supported services and characteristics on the user interface. displayGattServices(mBluetoothLeService.getSupportedGattServices()); @@ -160,6 +162,7 @@ public class DeviceControlActivity extends AppCompatActivity { }; private void clearUI() { + mExportString = null; mGattServicesList.setAdapter((SimpleExpandableListAdapter) null); mGattUUID.setText(R.string.no_data); mGattUUIDDesc.setText(R.string.no_data); @@ -172,125 +175,34 @@ public class DeviceControlActivity extends AppCompatActivity { // on the UI. private void displayGattServices(final List gattServices) { if (gattServices == null) return; - generateExportString(gattServices); + mExportString = mExporter.generateExportString( + mDevice.getName(), + mDevice.getAddress(), + gattServices); - String uuid = null; - final String unknownServiceString = getResources().getString(R.string.unknown_service); - final String unknownCharaString = getResources().getString(R.string.unknown_characteristic); - final List> gattServiceData = new ArrayList<>(); - final List>> gattCharacteristicData = new ArrayList<>(); - mGattCharacteristics = new ArrayList<>(); - - // Loops through available GATT Services. - for (final BluetoothGattService gattService : gattServices) { - final Map currentServiceData = new HashMap<>(); - uuid = gattService.getUuid().toString(); - currentServiceData.put(LIST_NAME, GattAttributeResolver.getAttributeName(uuid, unknownServiceString)); - currentServiceData.put(LIST_UUID, uuid); - gattServiceData.add(currentServiceData); - - final List> gattCharacteristicGroupData = new ArrayList<>(); - final List gattCharacteristics = gattService.getCharacteristics(); - final List charas = new ArrayList<>(); - - // Loops through available Characteristics. - for (final BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) { - charas.add(gattCharacteristic); - final Map currentCharaData = new HashMap<>(); - uuid = gattCharacteristic.getUuid().toString(); - currentCharaData.put(LIST_NAME, GattAttributeResolver.getAttributeName(uuid, unknownCharaString)); - currentCharaData.put(LIST_UUID, uuid); - gattCharacteristicGroupData.add(currentCharaData); - } - - mGattCharacteristics.add(charas); - gattCharacteristicData.add(gattCharacteristicGroupData); - } - - final SimpleExpandableListAdapter gattServiceAdapter = new SimpleExpandableListAdapter( - this, - gattServiceData, - android.R.layout.simple_expandable_list_item_2, - new String[]{LIST_NAME, LIST_UUID}, - new int[]{android.R.id.text1, android.R.id.text2}, - gattCharacteristicData, - android.R.layout.simple_expandable_list_item_2, - new String[]{LIST_NAME, LIST_UUID}, - new int[]{android.R.id.text1, android.R.id.text2} - ); - - mGattServicesList.setAdapter(gattServiceAdapter); + final GattDataAdapterFactory.GattDataAdapter adapter = GattDataAdapterFactory.createAdapter(this, gattServices); + mGattServicesList.setAdapter(adapter); invalidateOptionsMenu(); } - private void generateExportString(final List gattServices) { - final String unknownServiceString = getResources().getString(R.string.unknown_service); - final String unknownCharaString = getResources().getString(R.string.unknown_characteristic); - final StringBuilder exportBuilder = new StringBuilder(); - - exportBuilder.append("Device Name: "); - exportBuilder.append(mDeviceName); - exportBuilder.append('\n'); - exportBuilder.append("Device Address: "); - exportBuilder.append(mDeviceAddress); - exportBuilder.append('\n'); - exportBuilder.append('\n'); - - exportBuilder.append("Services:"); - exportBuilder.append("--------------------------"); - exportBuilder.append('\n'); - - String uuid = null; - for (final BluetoothGattService gattService : gattServices) { - uuid = gattService.getUuid().toString(); - - exportBuilder.append(GattAttributeResolver.getAttributeName(uuid, unknownServiceString)); - exportBuilder.append(" ("); - exportBuilder.append(uuid); - exportBuilder.append(')'); - exportBuilder.append('\n'); - - final List gattCharacteristics = gattService.getCharacteristics(); - for (final BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) { - uuid = gattCharacteristic.getUuid().toString(); - - exportBuilder.append('\t'); - exportBuilder.append(GattAttributeResolver.getAttributeName(uuid, unknownCharaString)); - exportBuilder.append(" ("); - exportBuilder.append(uuid); - exportBuilder.append(')'); - exportBuilder.append('\n'); - } - - exportBuilder.append('\n'); - exportBuilder.append('\n'); - } - - exportBuilder.append("--------------------------"); - exportBuilder.append('\n'); - - mExportString = exportBuilder.toString(); - } - @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_gatt_services); final Intent intent = getIntent(); - final BluetoothLeDevice device = intent.getParcelableExtra(EXTRA_DEVICE); - mDeviceName = device.getName(); - mDeviceAddress = device.getAddress(); - + mDevice = intent.getParcelableExtra(EXTRA_DEVICE); ButterKnife.bind(this); // Sets up UI references. - ((TextView) findViewById(R.id.device_address)).setText(mDeviceAddress); + ((TextView) findViewById(R.id.device_address)).setText(mDevice.getAddress()); mGattServicesList.setOnChildClickListener(servicesListClickListner); - getSupportActionBar().setTitle(mDeviceName); + getSupportActionBar().setTitle(mDevice.getName()); getSupportActionBar().setDisplayHomeAsUpEnabled(true); + mExporter = new Exporter(this); + final Intent gattServiceIntent = new Intent(this, BluetoothLeService.class); bindService(gattServiceIntent, mServiceConnection, BIND_AUTO_CREATE); } @@ -298,12 +210,26 @@ public class DeviceControlActivity extends AppCompatActivity { @Override public boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.gatt_services, menu); - if (mConnected) { - menu.findItem(R.id.menu_connect).setVisible(false); - menu.findItem(R.id.menu_disconnect).setVisible(true); - } else { - menu.findItem(R.id.menu_connect).setVisible(true); - menu.findItem(R.id.menu_disconnect).setVisible(false); + + switch (mCurrentState) { + + case DISCONNECTED: + menu.findItem(R.id.menu_connect).setVisible(true); + menu.findItem(R.id.menu_disconnect).setVisible(false); + menu.findItem(R.id.menu_refresh).setActionView(null); + break; + case CONNECTING: + menu.findItem(R.id.menu_connect).setVisible(false); + menu.findItem(R.id.menu_disconnect).setVisible(false); + menu.findItem(R.id.menu_refresh).setActionView(R.layout.actionbar_progress_indeterminate); + break; + case CONNECTED: + menu.findItem(R.id.menu_connect).setVisible(false); + menu.findItem(R.id.menu_disconnect).setVisible(true); + menu.findItem(R.id.menu_refresh).setActionView(null); + break; + default: + throw new IllegalStateException("Don't know how to handle: " + mCurrentState); } if (mExportString == null) { @@ -326,7 +252,7 @@ public class DeviceControlActivity extends AppCompatActivity { public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.menu_connect: - mBluetoothLeService.connect(mDeviceAddress); + mBluetoothLeService.connect(mDevice.getAddress()); return true; case R.id.menu_disconnect: mBluetoothLeService.disconnect(); @@ -336,7 +262,10 @@ public class DeviceControlActivity extends AppCompatActivity { return true; case R.id.menu_share: final Intent intent = new Intent(android.content.Intent.ACTION_SEND); - final String subject = getString(R.string.exporter_email_device_services_subject, mDeviceName, mDeviceAddress); + final String subject = getString( + R.string.exporter_email_device_services_subject, + mDevice.getName(), + mDevice.getAddress()); intent.setType("text/plain"); intent.putExtra(android.content.Intent.EXTRA_SUBJECT, subject); @@ -362,31 +291,41 @@ public class DeviceControlActivity extends AppCompatActivity { super.onResume(); registerReceiver(mGattUpdateReceiver, makeGattUpdateIntentFilter()); if (mBluetoothLeService != null) { - final boolean result = mBluetoothLeService.connect(mDeviceAddress); + final boolean result = mBluetoothLeService.connect(mDevice.getAddress()); Log.d(TAG, "Connect request result=" + result); } } - private void updateConnectionState(final int resourceId) { + private void updateConnectionState(final State state) { + mCurrentState = state; + runOnUiThread(new Runnable() { @Override public void run() { final int colourId; + final int resId; - switch (resourceId) { - case R.string.connected: + switch (state) { + case CONNECTED: colourId = android.R.color.holo_green_dark; + resId = R.string.connected; break; - case R.string.disconnected: + case DISCONNECTED: colourId = android.R.color.holo_red_dark; + resId = R.string.disconnected; + break; + case CONNECTING: + colourId = android.R.color.black; + resId = R.string.connecting; break; default: colourId = android.R.color.black; + resId = 0; break; } - mConnectionState.setText(resourceId); - mConnectionState.setTextColor(getResources().getColor(colourId)); + mConnectionState.setText(resId); + mConnectionState.setTextColor(ContextCompat.getColor(DeviceControlActivity.this, colourId)); } }); } @@ -397,6 +336,7 @@ public class DeviceControlActivity extends AppCompatActivity { intentFilter.addAction(BluetoothLeService.ACTION_GATT_DISCONNECTED); intentFilter.addAction(BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED); intentFilter.addAction(BluetoothLeService.ACTION_DATA_AVAILABLE); + intentFilter.addAction(BluetoothLeService.ACTION_GATT_CONNECTING); return intentFilter; } @@ -407,4 +347,16 @@ public class DeviceControlActivity extends AppCompatActivity { return string; } } + + public static Intent createIntent(final Context context, final BluetoothLeDevice device) { + final Intent intent = new Intent(context, DeviceControlActivity.class); + intent.putExtra(DeviceControlActivity.EXTRA_DEVICE, device); + return intent; + } + + private enum State { + DISCONNECTED, + CONNECTING, + CONNECTED + } } \ No newline at end of file diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/control/Exporter.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/control/Exporter.java new file mode 100644 index 0000000..7a8fb8f --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/control/Exporter.java @@ -0,0 +1,70 @@ +package uk.co.alt236.btlescan.ui.control; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattService; +import android.content.Context; + +import java.util.List; + +import uk.co.alt236.bluetoothlelib.resolvers.GattAttributeResolver; +import uk.co.alt236.btlescan.R; + +/*package*/ class Exporter { + private final Context mContext; + + public Exporter(final Context context) { + mContext = context.getApplicationContext(); + } + + public String generateExportString(final String deviceName, + final String deviceAddress, + final List gattServices) { + + final String unknownServiceString = mContext.getString(R.string.unknown_service); + final String unknownCharaString = mContext.getString(R.string.unknown_characteristic); + final StringBuilder exportBuilder = new StringBuilder(); + + exportBuilder.append("Device Name: "); + exportBuilder.append(deviceName); + exportBuilder.append('\n'); + exportBuilder.append("Device Address: "); + exportBuilder.append(deviceAddress); + exportBuilder.append('\n'); + exportBuilder.append('\n'); + + exportBuilder.append("Services:"); + exportBuilder.append("--------------------------"); + exportBuilder.append('\n'); + + String uuid = null; + for (final BluetoothGattService gattService : gattServices) { + uuid = gattService.getUuid().toString(); + + exportBuilder.append(GattAttributeResolver.getAttributeName(uuid, unknownServiceString)); + exportBuilder.append(" ("); + exportBuilder.append(uuid); + exportBuilder.append(')'); + exportBuilder.append('\n'); + + final List gattCharacteristics = gattService.getCharacteristics(); + for (final BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) { + uuid = gattCharacteristic.getUuid().toString(); + + exportBuilder.append('\t'); + exportBuilder.append(GattAttributeResolver.getAttributeName(uuid, unknownCharaString)); + exportBuilder.append(" ("); + exportBuilder.append(uuid); + exportBuilder.append(')'); + exportBuilder.append('\n'); + } + + exportBuilder.append('\n'); + exportBuilder.append('\n'); + } + + exportBuilder.append("--------------------------"); + exportBuilder.append('\n'); + + return exportBuilder.toString(); + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/control/GattDataAdapterFactory.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/control/GattDataAdapterFactory.java new file mode 100644 index 0000000..f9c71b1 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/control/GattDataAdapterFactory.java @@ -0,0 +1,94 @@ +package uk.co.alt236.btlescan.ui.control; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattService; +import android.content.Context; +import android.widget.SimpleExpandableListAdapter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import uk.co.alt236.bluetoothlelib.resolvers.GattAttributeResolver; +import uk.co.alt236.btlescan.R; + +/*package*/ class GattDataAdapterFactory { + private static final String LIST_NAME = "NAME"; + private static final String LIST_UUID = "UUID"; + + public static GattDataAdapter createAdapter(final Context context, + final List gattServices) { + + + final String unknownServiceString = context.getString(R.string.unknown_service); + final String unknownCharaString = context.getString(R.string.unknown_characteristic); + final List> gattServiceData = new ArrayList<>(); + final List>> gattCharacteristicData = new ArrayList<>(); + final List> fullGattCharacteristics = new ArrayList<>(); + + // Loops through available GATT Services. + String uuid; + for (final BluetoothGattService gattService : gattServices) { + final Map currentServiceData = new HashMap<>(); + uuid = gattService.getUuid().toString(); + currentServiceData.put(LIST_NAME, GattAttributeResolver.getAttributeName(uuid, unknownServiceString)); + currentServiceData.put(LIST_UUID, uuid); + gattServiceData.add(currentServiceData); + + final List> gattCharacteristicGroupData = new ArrayList<>(); + final List gattCharacteristics = gattService.getCharacteristics(); + final List charas = new ArrayList<>(); + + // Loops through available Characteristics. + for (final BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) { + charas.add(gattCharacteristic); + final Map currentCharaData = new HashMap<>(); + uuid = gattCharacteristic.getUuid().toString(); + currentCharaData.put(LIST_NAME, GattAttributeResolver.getAttributeName(uuid, unknownCharaString)); + currentCharaData.put(LIST_UUID, uuid); + gattCharacteristicGroupData.add(currentCharaData); + } + + fullGattCharacteristics.add(charas); + gattCharacteristicData.add(gattCharacteristicGroupData); + } + + return new GattDataAdapter( + context, + fullGattCharacteristics, + gattServiceData, + android.R.layout.simple_expandable_list_item_2, + new String[]{LIST_NAME, LIST_UUID}, + new int[]{android.R.id.text1, android.R.id.text2}, + gattCharacteristicData, + android.R.layout.simple_expandable_list_item_2, + new String[]{LIST_NAME, LIST_UUID}, + new int[]{android.R.id.text1, android.R.id.text2} + ); + } + + + public static class GattDataAdapter extends SimpleExpandableListAdapter { + + private final List> mGattCharacteristics; + + public GattDataAdapter(Context context, + List> gattCharacteristics, + List> groupData, + int groupLayout, String[] groupFrom, + int[] groupTo, + List>> childData, + int childLayout, + String[] childFrom, + int[] childTo) { + + super(context, groupData, groupLayout, groupFrom, groupTo, childData, childLayout, childFrom, childTo); + mGattCharacteristics = gattCharacteristics; + } + + public BluetoothGattCharacteristic getBluetoothGattCharacteristic(final int groupPosition, final int childPosition) { + return mGattCharacteristics.get(groupPosition).get(childPosition); + } + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/DetailsRecyclerAdapter.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/DetailsRecyclerAdapter.java new file mode 100644 index 0000000..fa094a5 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/DetailsRecyclerAdapter.java @@ -0,0 +1,25 @@ +package uk.co.alt236.btlescan.ui.details; + +import java.util.List; + +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseRecyclerViewAdapter; +import uk.co.alt236.btlescan.ui.common.recyclerview.RecyclerViewBinderCore; +import uk.co.alt236.btlescan.ui.common.recyclerview.RecyclerViewItem; + +/*package*/ class DetailsRecyclerAdapter extends BaseRecyclerViewAdapter { + public DetailsRecyclerAdapter(RecyclerViewBinderCore core, List items) { + super(core, items); + } + +// private static List validate(RecyclerViewBinderCore core, List items) { +// final List retVal = new ArrayList<>(); +// +// for (final RecyclerViewItem item : items) { +// if (core.getViewType(item) != RecyclerViewBinderCore.INVALID_VIEWTYPE) { +// retVal.add(item); +// } +// } +// +// return retVal; +// } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/DeviceDetailsActivity.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/DeviceDetailsActivity.java new file mode 100644 index 0000000..12cd7d4 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/DeviceDetailsActivity.java @@ -0,0 +1,125 @@ +package uk.co.alt236.btlescan.ui.details; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.Menu; +import android.view.MenuItem; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import butterknife.Bind; +import butterknife.ButterKnife; +import uk.co.alt236.bluetoothlelib.device.BluetoothLeDevice; +import uk.co.alt236.bluetoothlelib.device.adrecord.AdRecord; +import uk.co.alt236.bluetoothlelib.device.beacon.BeaconType; +import uk.co.alt236.bluetoothlelib.device.beacon.BeaconUtils; +import uk.co.alt236.bluetoothlelib.device.beacon.ibeacon.IBeaconManufacturerData; +import uk.co.alt236.bluetoothlelib.util.ByteUtils; +import uk.co.alt236.btlescan.R; +import uk.co.alt236.btlescan.ui.common.Navigation; +import uk.co.alt236.btlescan.ui.common.recyclerview.RecyclerViewBinderCore; +import uk.co.alt236.btlescan.ui.common.recyclerview.RecyclerViewItem; +import uk.co.alt236.btlescan.ui.details.recyclerview.model.AdRecordItem; +import uk.co.alt236.btlescan.ui.details.recyclerview.model.DeviceInfoItem; +import uk.co.alt236.btlescan.ui.details.recyclerview.model.HeaderItem; +import uk.co.alt236.btlescan.ui.details.recyclerview.model.IBeaconItem; +import uk.co.alt236.btlescan.ui.details.recyclerview.model.RssiItem; +import uk.co.alt236.btlescan.ui.details.recyclerview.model.TextItem; + +public class DeviceDetailsActivity extends AppCompatActivity { + private static final String EXTRA_DEVICE = DeviceDetailsActivity.class.getName() + ".EXTRA_DEVICE"; + private static final int LAYOUT_ID = R.layout.activity_details; + + @Bind(R.id.recycler) + protected RecyclerView mRecycler; + private BluetoothLeDevice mDevice; + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(LAYOUT_ID); + ButterKnife.bind(this); + + mRecycler.setLayoutManager(new LinearLayoutManager(this)); + mDevice = getIntent().getParcelableExtra(EXTRA_DEVICE); + + getSupportActionBar().setTitle(mDevice.getName()); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + pupulateDetails(mDevice); + } + + @Override + public boolean onCreateOptionsMenu(final Menu menu) { + getMenuInflater().inflate(R.menu.details, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_connect: + new Navigation(this).startControlActivity(mDevice); + return true; + case android.R.id.home: + onBackPressed(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void pupulateDetails(final BluetoothLeDevice device) { + final DetailsRecyclerAdapter detailsRecyclerAdapter; + + final List list = new ArrayList<>(); + + if (device == null) { + list.add(new HeaderItem(getString(R.string.header_device_info))); + list.add(new TextItem(getString(R.string.invalid_device_data))); + } else { + list.add(new HeaderItem(getString(R.string.header_device_info))); + list.add(new DeviceInfoItem(device)); + + list.add(new HeaderItem(getString(R.string.header_rssi_info))); + list.add(new RssiItem(device)); + + list.add(new HeaderItem(getString(R.string.header_scan_record))); + list.add(new TextItem(ByteUtils.byteArrayToHexString(device.getScanRecord()))); + + final Collection adRecords = device.getAdRecordStore().getRecordsAsCollection(); + if (adRecords.size() > 0) { + list.add(new HeaderItem(getString(R.string.header_raw_ad_records))); + + for (final AdRecord record : adRecords) { + + final String title = "#" + record.getType() + " " + record.getHumanReadableType(); + list.add(new AdRecordItem(title, record)); + } + } + + final boolean isIBeacon = BeaconUtils.getBeaconType(device) == BeaconType.IBEACON; + if (isIBeacon) { + final IBeaconManufacturerData iBeaconData = new IBeaconManufacturerData(device); + list.add(new HeaderItem(getString(R.string.header_ibeacon_data))); + list.add(new IBeaconItem(iBeaconData)); + } + + } + + final RecyclerViewBinderCore core = RecyclerViewCoreFactory.create(this); + mRecycler.setAdapter(new DetailsRecyclerAdapter(core, list)); + } + + public static Intent createIntent(Context context, BluetoothLeDevice device) { + final Intent intent = new Intent(context, DeviceDetailsActivity.class); + intent.putExtra(DeviceDetailsActivity.EXTRA_DEVICE, device); + + return intent; + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/RecyclerViewCoreFactory.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/RecyclerViewCoreFactory.java new file mode 100644 index 0000000..c0dca9b --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/RecyclerViewCoreFactory.java @@ -0,0 +1,35 @@ +package uk.co.alt236.btlescan.ui.details; + +import android.content.Context; + +import uk.co.alt236.btlescan.R; +import uk.co.alt236.btlescan.ui.common.recyclerview.RecyclerViewBinderCore; +import uk.co.alt236.btlescan.ui.details.recyclerview.binder.AdRecordBinder; +import uk.co.alt236.btlescan.ui.details.recyclerview.binder.DeviceInfoBinder; +import uk.co.alt236.btlescan.ui.details.recyclerview.binder.HeaderBinder; +import uk.co.alt236.btlescan.ui.details.recyclerview.binder.IBeaconBinder; +import uk.co.alt236.btlescan.ui.details.recyclerview.binder.RssiBinder; +import uk.co.alt236.btlescan.ui.details.recyclerview.binder.TextBinder; +import uk.co.alt236.btlescan.ui.details.recyclerview.holder.AdRecordHolder; +import uk.co.alt236.btlescan.ui.details.recyclerview.holder.DeviceInfoHolder; +import uk.co.alt236.btlescan.ui.details.recyclerview.holder.HeaderHolder; +import uk.co.alt236.btlescan.ui.details.recyclerview.holder.IBeaconHolder; +import uk.co.alt236.btlescan.ui.details.recyclerview.holder.RssiInfoHolder; +import uk.co.alt236.btlescan.ui.details.recyclerview.holder.TextHolder; + +/*protected*/ final class RecyclerViewCoreFactory { + + public static RecyclerViewBinderCore create(final Context context) { + final RecyclerViewBinderCore core = new RecyclerViewBinderCore(); + + core.add(new TextBinder(context), TextHolder.class, R.layout.list_item_view_textview); + core.add(new HeaderBinder(context), HeaderHolder.class, R.layout.list_item_view_header); + core.add(new AdRecordBinder(context), AdRecordHolder.class, R.layout.list_item_view_adrecord); + core.add(new RssiBinder(context), RssiInfoHolder.class, R.layout.list_item_view_rssi_info); + core.add(new DeviceInfoBinder(context), DeviceInfoHolder.class, R.layout.list_item_view_device_info); + core.add(new IBeaconBinder(context), IBeaconHolder.class, R.layout.list_item_view_ibeacon_details); + + return core; + } + +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/binder/AdRecordBinder.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/binder/AdRecordBinder.java new file mode 100644 index 0000000..eb145b5 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/binder/AdRecordBinder.java @@ -0,0 +1,38 @@ +package uk.co.alt236.btlescan.ui.details.recyclerview.binder; + +import android.content.Context; + +import uk.co.alt236.bluetoothlelib.util.ByteUtils; +import uk.co.alt236.btlescan.R; +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseViewBinder; +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseViewHolder; +import uk.co.alt236.btlescan.ui.common.recyclerview.RecyclerViewItem; +import uk.co.alt236.btlescan.ui.details.recyclerview.holder.AdRecordHolder; +import uk.co.alt236.btlescan.ui.details.recyclerview.model.AdRecordItem; + +public class AdRecordBinder extends BaseViewBinder { + + public AdRecordBinder(Context context) { + super(context); + } + + @Override + public void bind(BaseViewHolder holder, AdRecordItem item) { + final AdRecordHolder actualHolder = (AdRecordHolder) holder; + + actualHolder.getTitleTextView().setText(item.getTitle()); + + actualHolder.getStringTextView().setText( + getContext().getString(R.string.formatter_single_quoted_string, + item.getDataAsString())); + + actualHolder.getArrayTextView().setText( + getContext().getString(R.string.formatter_single_quoted_string, + ByteUtils.byteArrayToHexString(item.getData()))); + } + + @Override + public boolean canBind(RecyclerViewItem item) { + return item instanceof AdRecordItem; + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/binder/DeviceInfoBinder.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/binder/DeviceInfoBinder.java new file mode 100644 index 0000000..5b3ba90 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/binder/DeviceInfoBinder.java @@ -0,0 +1,57 @@ +package uk.co.alt236.btlescan.ui.details.recyclerview.binder; + +import android.content.Context; + +import uk.co.alt236.bluetoothlelib.device.BluetoothService; +import uk.co.alt236.btlescan.R; +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseViewBinder; +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseViewHolder; +import uk.co.alt236.btlescan.ui.common.recyclerview.RecyclerViewItem; +import uk.co.alt236.btlescan.ui.details.recyclerview.holder.DeviceInfoHolder; +import uk.co.alt236.btlescan.ui.details.recyclerview.model.DeviceInfoItem; + +public class DeviceInfoBinder extends BaseViewBinder { + + public DeviceInfoBinder(Context context) { + super(context); + } + + @Override + public void bind(BaseViewHolder holder, DeviceInfoItem item) { + final DeviceInfoHolder actualHolder = (DeviceInfoHolder) holder; + + actualHolder.getName().setText(item.getName()); + actualHolder.getAddress().setText(item.getAddress()); + actualHolder.getDeviceClass().setText(item.getBluetoothDeviceClassName()); + actualHolder.getMajorClass().setText(item.getBluetoothDeviceMajorClassName()); + actualHolder.getBondingState().setText(item.getBluetoothDeviceBondState()); + actualHolder.getServices().setText(createSupportedDevicesString(item)); + } + + + private String createSupportedDevicesString(DeviceInfoItem item) { + final String retVal; + + if (item.getBluetoothDeviceKnownSupportedServices().isEmpty()) { + retVal = getContext().getString(R.string.no_known_services); + } else { + final StringBuilder sb = new StringBuilder(); + + for (final BluetoothService service : item.getBluetoothDeviceKnownSupportedServices()) { + if (sb.length() > 0) { + sb.append(", "); + } + + sb.append(service); + } + retVal = sb.toString(); + } + + return retVal; + } + + @Override + public boolean canBind(RecyclerViewItem item) { + return item instanceof DeviceInfoItem; + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/binder/HeaderBinder.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/binder/HeaderBinder.java new file mode 100644 index 0000000..1144977 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/binder/HeaderBinder.java @@ -0,0 +1,27 @@ +package uk.co.alt236.btlescan.ui.details.recyclerview.binder; + +import android.content.Context; + +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseViewBinder; +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseViewHolder; +import uk.co.alt236.btlescan.ui.common.recyclerview.RecyclerViewItem; +import uk.co.alt236.btlescan.ui.details.recyclerview.holder.HeaderHolder; +import uk.co.alt236.btlescan.ui.details.recyclerview.model.HeaderItem; + +public class HeaderBinder extends BaseViewBinder { + + public HeaderBinder(Context context) { + super(context); + } + + @Override + public void bind(BaseViewHolder holder, HeaderItem item) { + final HeaderHolder actualHolder = (HeaderHolder) holder; + actualHolder.getTextView().setText(item.getText()); + } + + @Override + public boolean canBind(RecyclerViewItem item) { + return item instanceof HeaderItem; + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/binder/IBeaconBinder.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/binder/IBeaconBinder.java new file mode 100644 index 0000000..ed3d2cf --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/binder/IBeaconBinder.java @@ -0,0 +1,66 @@ +package uk.co.alt236.btlescan.ui.details.recyclerview.binder; + +import android.content.Context; + +import java.util.Locale; + +import uk.co.alt236.bluetoothlelib.resolvers.CompanyIdentifierResolver; +import uk.co.alt236.btlescan.R; +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseViewBinder; +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseViewHolder; +import uk.co.alt236.btlescan.ui.common.recyclerview.RecyclerViewItem; +import uk.co.alt236.btlescan.ui.details.recyclerview.holder.IBeaconHolder; +import uk.co.alt236.btlescan.ui.details.recyclerview.model.IBeaconItem; +import uk.co.alt236.btlescan.util.TimeFormatter; + +public class IBeaconBinder extends BaseViewBinder { + private static final String STRING_FORMAT = "%s (%s)"; + + public IBeaconBinder(Context context) { + super(context); + } + + private static String formatTime(final long time) { + return TimeFormatter.getIsoDateTime(time); + } + + private static String getWithHexEncode(final String first, final int value) { + return createLine(first, hexEncode(value)); + } + + private static String getWithHexEncode(final int value) { + return createLine(String.valueOf(value), hexEncode(value)); + } + + private static String createLine(final String first, final String second) { + return String.format(Locale.US, STRING_FORMAT, first, second); + } + + private static String hexEncode(final int integer) { + return "0x" + Integer.toHexString(integer).toUpperCase(Locale.US); + } + + @Override + public void bind(BaseViewHolder holder, IBeaconItem item) { + final IBeaconHolder actualHolder = (IBeaconHolder) holder; + + + final String companyName = CompanyIdentifierResolver.getCompanyName( + item.getCompanyIdentifier(), + getContext().getString(R.string.unknown)); + + actualHolder.getCompanyId().setText( + getWithHexEncode(companyName, item.getCompanyIdentifier())); + + actualHolder.getAdvert().setText(getWithHexEncode(item.getIBeaconAdvertisement())); + actualHolder.getUuid().setText(item.getUuid()); + actualHolder.getMajor().setText(getWithHexEncode(item.getMajor())); + actualHolder.getMinor().setText(getWithHexEncode(item.getMinor())); + actualHolder.getTxPower().setText(getWithHexEncode(item.getCalibratedTxPower())); + } + + @Override + public boolean canBind(RecyclerViewItem item) { + return item instanceof IBeaconItem; + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/binder/RssiBinder.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/binder/RssiBinder.java new file mode 100644 index 0000000..007c70e --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/binder/RssiBinder.java @@ -0,0 +1,46 @@ +package uk.co.alt236.btlescan.ui.details.recyclerview.binder; + +import android.content.Context; + +import uk.co.alt236.btlescan.R; +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseViewBinder; +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseViewHolder; +import uk.co.alt236.btlescan.ui.common.recyclerview.RecyclerViewItem; +import uk.co.alt236.btlescan.ui.details.recyclerview.holder.RssiInfoHolder; +import uk.co.alt236.btlescan.ui.details.recyclerview.model.RssiItem; +import uk.co.alt236.btlescan.util.TimeFormatter; + +public class RssiBinder extends BaseViewBinder { + + public RssiBinder(Context context) { + super(context); + } + + private static String formatTime(final long time) { + return TimeFormatter.getIsoDateTime(time); + } + + @Override + public void bind(BaseViewHolder holder, RssiItem item) { + final RssiInfoHolder actualHolder = (RssiInfoHolder) holder; + + actualHolder.getFirstTimestamp().setText(formatTime(item.getFirstTimestamp())); + actualHolder.getFirstRssi().setText(formatRssi(item.getFirstRssi())); + actualHolder.getLastTimestamp().setText(formatTime(item.getTimestamp())); + actualHolder.getLastRssi().setText(formatRssi(item.getRssi())); + actualHolder.getRunningAverageRssi().setText(formatRssi(item.getRunningAverageRssi())); + } + + @Override + public boolean canBind(RecyclerViewItem item) { + return item instanceof RssiItem; + } + + private String formatRssi(final double rssi) { + return getContext().getString(R.string.formatter_db, String.valueOf(rssi)); + } + + private String formatRssi(final int rssi) { + return getContext().getString(R.string.formatter_db, String.valueOf(rssi)); + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/binder/TextBinder.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/binder/TextBinder.java new file mode 100644 index 0000000..8a68ee6 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/binder/TextBinder.java @@ -0,0 +1,27 @@ +package uk.co.alt236.btlescan.ui.details.recyclerview.binder; + +import android.content.Context; + +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseViewBinder; +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseViewHolder; +import uk.co.alt236.btlescan.ui.common.recyclerview.RecyclerViewItem; +import uk.co.alt236.btlescan.ui.details.recyclerview.holder.TextHolder; +import uk.co.alt236.btlescan.ui.details.recyclerview.model.TextItem; + +public class TextBinder extends BaseViewBinder { + + public TextBinder(Context context) { + super(context); + } + + @Override + public void bind(BaseViewHolder holder, TextItem item) { + final TextHolder actualHolder = (TextHolder) holder; + actualHolder.getTextView().setText(item.getText()); + } + + @Override + public boolean canBind(RecyclerViewItem item) { + return item instanceof TextItem; + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/holder/AdRecordHolder.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/holder/AdRecordHolder.java new file mode 100644 index 0000000..20e2246 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/holder/AdRecordHolder.java @@ -0,0 +1,35 @@ +package uk.co.alt236.btlescan.ui.details.recyclerview.holder; + +import android.view.View; +import android.widget.TextView; + +import uk.co.alt236.btlescan.R; +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseViewHolder; +import uk.co.alt236.btlescan.ui.details.recyclerview.model.AdRecordItem; + +public class AdRecordHolder extends BaseViewHolder { + + private final TextView mStringTextView; + private final TextView mArrayTextView; + private final TextView mTitleTextView; + + public AdRecordHolder(View itemView) { + super(itemView); + + mStringTextView = (TextView) itemView.findViewById(R.id.data_as_string); + mArrayTextView = (TextView) itemView.findViewById(R.id.data_as_array); + mTitleTextView = (TextView) itemView.findViewById(R.id.title); + } + + public TextView getStringTextView() { + return mStringTextView; + } + + public TextView getArrayTextView() { + return mArrayTextView; + } + + public TextView getTitleTextView() { + return mTitleTextView; + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/holder/DeviceInfoHolder.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/holder/DeviceInfoHolder.java new file mode 100644 index 0000000..62853d4 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/holder/DeviceInfoHolder.java @@ -0,0 +1,53 @@ +package uk.co.alt236.btlescan.ui.details.recyclerview.holder; + +import android.view.View; +import android.widget.TextView; + +import uk.co.alt236.btlescan.R; +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseViewHolder; +import uk.co.alt236.btlescan.ui.details.recyclerview.model.DeviceInfoItem; + +public class DeviceInfoHolder extends BaseViewHolder { + + private final TextView mName; + private final TextView mAddress; + private final TextView mClass; + private final TextView mMajorClass; + private final TextView mServices; + private final TextView mBondingState; + + public DeviceInfoHolder(View itemView) { + super(itemView); + + mName = (TextView) itemView.findViewById(R.id.deviceName); + mAddress = (TextView) itemView.findViewById(R.id.deviceAddress); + mClass = (TextView) itemView.findViewById(R.id.deviceClass); + mMajorClass = (TextView) itemView.findViewById(R.id.deviceMajorClass); + mServices = (TextView) itemView.findViewById(R.id.deviceServiceList); + mBondingState = (TextView) itemView.findViewById(R.id.deviceBondingState); + } + + public TextView getName() { + return mName; + } + + public TextView getAddress() { + return mAddress; + } + + public TextView getDeviceClass() { + return mClass; + } + + public TextView getMajorClass() { + return mMajorClass; + } + + public TextView getServices() { + return mServices; + } + + public TextView getBondingState() { + return mBondingState; + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/holder/HeaderHolder.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/holder/HeaderHolder.java new file mode 100644 index 0000000..4e3d10c --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/holder/HeaderHolder.java @@ -0,0 +1,23 @@ +package uk.co.alt236.btlescan.ui.details.recyclerview.holder; + +import android.view.View; +import android.widget.TextView; + +import uk.co.alt236.btlescan.R; +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseViewHolder; +import uk.co.alt236.btlescan.ui.details.recyclerview.model.HeaderItem; + +public class HeaderHolder extends BaseViewHolder { + + private final TextView mText; + + public HeaderHolder(View itemView) { + super(itemView); + + mText = (TextView) itemView.findViewById(R.id.text); + } + + public TextView getTextView() { + return mText; + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/holder/IBeaconHolder.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/holder/IBeaconHolder.java new file mode 100644 index 0000000..2167615 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/holder/IBeaconHolder.java @@ -0,0 +1,53 @@ +package uk.co.alt236.btlescan.ui.details.recyclerview.holder; + +import android.view.View; +import android.widget.TextView; + +import uk.co.alt236.btlescan.R; +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseViewHolder; +import uk.co.alt236.btlescan.ui.details.recyclerview.model.IBeaconItem; + +public class IBeaconHolder extends BaseViewHolder { + + private final TextView mCompanyId; + private final TextView mAdvert; + private final TextView mUuid; + private final TextView mMajor; + private final TextView mMinor; + private final TextView mTxPower; + + public IBeaconHolder(View itemView) { + super(itemView); + + mCompanyId = (TextView) itemView.findViewById(R.id.companyId); + mAdvert = (TextView) itemView.findViewById(R.id.advertisement); + mUuid = (TextView) itemView.findViewById(R.id.uuid); + mMajor = (TextView) itemView.findViewById(R.id.major); + mMinor = (TextView) itemView.findViewById(R.id.minor); + mTxPower = (TextView) itemView.findViewById(R.id.txpower); + } + + public TextView getCompanyId() { + return mCompanyId; + } + + public TextView getAdvert() { + return mAdvert; + } + + public TextView getUuid() { + return mUuid; + } + + public TextView getMajor() { + return mMajor; + } + + public TextView getMinor() { + return mMinor; + } + + public TextView getTxPower() { + return mTxPower; + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/holder/RssiInfoHolder.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/holder/RssiInfoHolder.java new file mode 100644 index 0000000..b36125b --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/holder/RssiInfoHolder.java @@ -0,0 +1,47 @@ +package uk.co.alt236.btlescan.ui.details.recyclerview.holder; + +import android.view.View; +import android.widget.TextView; + +import uk.co.alt236.btlescan.R; +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseViewHolder; +import uk.co.alt236.btlescan.ui.details.recyclerview.model.RssiItem; + +public class RssiInfoHolder extends BaseViewHolder { + + private final TextView mTvFirstTimestamp; + private final TextView mTvFirstRssi; + private final TextView mTvLastTimestamp; + private final TextView mTvLastRssi; + private final TextView mTvRunningAverageRssi; + + public RssiInfoHolder(View itemView) { + super(itemView); + + mTvFirstTimestamp = (TextView) itemView.findViewById(R.id.firstTimestamp); + mTvFirstRssi = (TextView) itemView.findViewById(R.id.firstRssi); + mTvLastTimestamp = (TextView) itemView.findViewById(R.id.lastTimestamp); + mTvLastRssi = (TextView) itemView.findViewById(R.id.lastRssi); + mTvRunningAverageRssi = (TextView) itemView.findViewById(R.id.runningAverageRssi); + } + + public TextView getFirstTimestamp() { + return mTvFirstTimestamp; + } + + public TextView getFirstRssi() { + return mTvFirstRssi; + } + + public TextView getLastTimestamp() { + return mTvLastTimestamp; + } + + public TextView getLastRssi() { + return mTvLastRssi; + } + + public TextView getRunningAverageRssi() { + return mTvRunningAverageRssi; + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/holder/TextHolder.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/holder/TextHolder.java new file mode 100644 index 0000000..48f2e96 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/holder/TextHolder.java @@ -0,0 +1,23 @@ +package uk.co.alt236.btlescan.ui.details.recyclerview.holder; + +import android.view.View; +import android.widget.TextView; + +import uk.co.alt236.btlescan.R; +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseViewHolder; +import uk.co.alt236.btlescan.ui.details.recyclerview.model.TextItem; + +public class TextHolder extends BaseViewHolder { + + private final TextView mText; + + public TextHolder(View itemView) { + super(itemView); + + mText = (TextView) itemView.findViewById(R.id.text); + } + + public TextView getTextView() { + return mText; + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/model/AdRecordItem.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/model/AdRecordItem.java new file mode 100644 index 0000000..68be76e --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/model/AdRecordItem.java @@ -0,0 +1,31 @@ +package uk.co.alt236.btlescan.ui.details.recyclerview.model; + +import uk.co.alt236.bluetoothlelib.device.adrecord.AdRecord; +import uk.co.alt236.bluetoothlelib.util.AdRecordUtils; +import uk.co.alt236.btlescan.ui.common.recyclerview.RecyclerViewItem; + +public class AdRecordItem implements RecyclerViewItem { + + private final String mTitle; + private final byte[] mData; + private final String mDataAsString; + + public AdRecordItem(final String title, + final AdRecord record) { + mTitle = title; + mData = record.getData(); + mDataAsString = AdRecordUtils.getRecordDataAsString(record); + } + + public String getTitle() { + return mTitle; + } + + public byte[] getData() { + return mData; + } + + public String getDataAsString() { + return mDataAsString; + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/model/DeviceInfoItem.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/model/DeviceInfoItem.java new file mode 100644 index 0000000..e2541d9 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/model/DeviceInfoItem.java @@ -0,0 +1,40 @@ +package uk.co.alt236.btlescan.ui.details.recyclerview.model; + +import java.util.Set; + +import uk.co.alt236.bluetoothlelib.device.BluetoothLeDevice; +import uk.co.alt236.bluetoothlelib.device.BluetoothService; +import uk.co.alt236.btlescan.ui.common.recyclerview.RecyclerViewItem; + +public class DeviceInfoItem implements RecyclerViewItem { + + private final BluetoothLeDevice mDevice; + + public DeviceInfoItem(BluetoothLeDevice device) { + mDevice = device; + } + + public Set getBluetoothDeviceKnownSupportedServices() { + return mDevice.getBluetoothDeviceKnownSupportedServices(); + } + + public String getBluetoothDeviceBondState() { + return mDevice.getBluetoothDeviceBondState(); + } + + public String getBluetoothDeviceMajorClassName() { + return mDevice.getBluetoothDeviceMajorClassName(); + } + + public String getBluetoothDeviceClassName() { + return mDevice.getBluetoothDeviceClassName(); + } + + public String getAddress() { + return mDevice.getAddress(); + } + + public String getName() { + return mDevice.getName(); + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/model/HeaderItem.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/model/HeaderItem.java new file mode 100644 index 0000000..46b7db6 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/model/HeaderItem.java @@ -0,0 +1,15 @@ +package uk.co.alt236.btlescan.ui.details.recyclerview.model; + +import uk.co.alt236.btlescan.ui.common.recyclerview.RecyclerViewItem; + +public class HeaderItem implements RecyclerViewItem { + private final CharSequence mText; + + public HeaderItem(CharSequence text) { + mText = text; + } + + public CharSequence getText() { + return mText; + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/model/IBeaconItem.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/model/IBeaconItem.java new file mode 100644 index 0000000..9db95de --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/model/IBeaconItem.java @@ -0,0 +1,47 @@ +package uk.co.alt236.btlescan.ui.details.recyclerview.model; + +import uk.co.alt236.bluetoothlelib.device.beacon.ibeacon.IBeaconManufacturerData; +import uk.co.alt236.btlescan.ui.common.recyclerview.RecyclerViewItem; + +public class IBeaconItem implements RecyclerViewItem { + + private final int mMajor; + private final int mMinor; + private final String mUuid; + private final int mCompanyIdentifier; + private final int mIBeaconAdvertisement; + private final int mCalibratedTxPower; + + public IBeaconItem(final IBeaconManufacturerData iBeaconData) { + mMajor = iBeaconData.getMajor(); + mMinor = iBeaconData.getMinor(); + mUuid = iBeaconData.getUUID(); + mCompanyIdentifier = iBeaconData.getCompanyIdentifier(); + mIBeaconAdvertisement = iBeaconData.getIBeaconAdvertisement(); + mCalibratedTxPower = iBeaconData.getCalibratedTxPower(); + } + + public int getCompanyIdentifier() { + return mCompanyIdentifier; + } + + public int getMajor() { + return mMajor; + } + + public int getMinor() { + return mMinor; + } + + public String getUuid() { + return mUuid; + } + + public int getIBeaconAdvertisement() { + return mIBeaconAdvertisement; + } + + public int getCalibratedTxPower() { + return mCalibratedTxPower; + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/model/RssiItem.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/model/RssiItem.java new file mode 100644 index 0000000..0271a50 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/model/RssiItem.java @@ -0,0 +1,33 @@ +package uk.co.alt236.btlescan.ui.details.recyclerview.model; + +import uk.co.alt236.bluetoothlelib.device.BluetoothLeDevice; +import uk.co.alt236.btlescan.ui.common.recyclerview.RecyclerViewItem; + +public class RssiItem implements RecyclerViewItem { + + private final BluetoothLeDevice mDevice; + + public RssiItem(BluetoothLeDevice device) { + mDevice = device; + } + + public int getRssi() { + return mDevice.getRssi(); + } + + public double getRunningAverageRssi() { + return mDevice.getRunningAverageRssi(); + } + + public int getFirstRssi() { + return mDevice.getFirstRssi(); + } + + public long getFirstTimestamp() { + return mDevice.getFirstTimestamp(); + } + + public long getTimestamp() { + return mDevice.getTimestamp(); + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/model/TextItem.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/model/TextItem.java new file mode 100644 index 0000000..730fcc9 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/details/recyclerview/model/TextItem.java @@ -0,0 +1,15 @@ +package uk.co.alt236.btlescan.ui.details.recyclerview.model; + +import uk.co.alt236.btlescan.ui.common.recyclerview.RecyclerViewItem; + +public class TextItem implements RecyclerViewItem { + private final CharSequence mText; + + public TextItem(CharSequence text) { + mText = text; + } + + public CharSequence getText() { + return mText; + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/util/CsvWriterHelper.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/CsvWriterHelper.java similarity index 88% rename from sample_app/src/main/java/uk/co/alt236/btlescan/util/CsvWriterHelper.java rename to sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/CsvWriterHelper.java index f8bff7d..c73b4c7 100644 --- a/sample_app/src/main/java/uk/co/alt236/btlescan/util/CsvWriterHelper.java +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/CsvWriterHelper.java @@ -1,6 +1,6 @@ -package uk.co.alt236.btlescan.util; +package uk.co.alt236.btlescan.ui.main; -public class CsvWriterHelper { +/*package*/ class CsvWriterHelper { private static final String QUOTE = "\""; public static String addStuff(final Integer text) { diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/DeviceRecyclerAdapter.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/DeviceRecyclerAdapter.java new file mode 100644 index 0000000..57dc71a --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/DeviceRecyclerAdapter.java @@ -0,0 +1,10 @@ +package uk.co.alt236.btlescan.ui.main; + +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseRecyclerViewAdapter; +import uk.co.alt236.btlescan.ui.common.recyclerview.RecyclerViewBinderCore; + +/*package*/ class DeviceRecyclerAdapter extends BaseRecyclerViewAdapter { + public DeviceRecyclerAdapter(RecyclerViewBinderCore core) { + super(core); + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/DialogFactory.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/DialogFactory.java new file mode 100644 index 0000000..6c786a1 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/DialogFactory.java @@ -0,0 +1,47 @@ +package uk.co.alt236.btlescan.ui.main; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.support.v7.app.AlertDialog; +import android.text.SpannableString; +import android.text.method.LinkMovementMethod; +import android.text.util.Linkify; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import uk.co.alt236.btlescan.R; + +/*package*/ final class DialogFactory { + + private DialogFactory() { + // NOOP + } + + public static Dialog createAboutDialog(final Context context) { + final View view = LayoutInflater.from(context).inflate(R.layout.dialog_textview, null); + final TextView textView = (TextView) view.findViewById(R.id.text); + + final SpannableString text = new SpannableString(context.getString(R.string.about_dialog_text)); + + textView.setText(text); + textView.setAutoLinkMask(Activity.RESULT_OK); + textView.setMovementMethod(LinkMovementMethod.getInstance()); + + Linkify.addLinks(text, Linkify.ALL); + + final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { + public void onClick(final DialogInterface dialog, final int id) { + } + }; + + return new AlertDialog.Builder(context) + .setTitle(R.string.menu_about) + .setCancelable(false) + .setPositiveButton(android.R.string.ok, listener) + .setView(view) + .create(); + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/activities/MainActivity.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/MainActivity.java similarity index 57% rename from sample_app/src/main/java/uk/co/alt236/btlescan/activities/MainActivity.java rename to sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/MainActivity.java index 153f522..f7fcdaf 100644 --- a/sample_app/src/main/java/uk/co/alt236/btlescan/activities/MainActivity.java +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/MainActivity.java @@ -1,33 +1,42 @@ -package uk.co.alt236.btlescan.activities; +package uk.co.alt236.btlescan.ui.main; -import android.app.AlertDialog; +import android.Manifest; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; -import android.content.DialogInterface; -import android.content.Intent; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v7.app.AppCompatActivity; -import android.text.SpannableString; -import android.text.method.LinkMovementMethod; -import android.text.util.Linkify; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.widget.AdapterView; -import android.widget.ListView; import android.widget.TextView; +import android.widget.Toast; + +import com.anthonycr.grant.PermissionsManager; +import com.anthonycr.grant.PermissionsResultAction; + +import java.util.ArrayList; +import java.util.List; import butterknife.Bind; import butterknife.ButterKnife; import uk.co.alt236.bluetoothlelib.device.BluetoothLeDevice; +import uk.co.alt236.bluetoothlelib.device.beacon.BeaconType; +import uk.co.alt236.bluetoothlelib.device.beacon.BeaconUtils; +import uk.co.alt236.bluetoothlelib.device.beacon.ibeacon.IBeaconDevice; import uk.co.alt236.btlescan.R; -import uk.co.alt236.btlescan.adapters.LeDeviceListAdapter; import uk.co.alt236.btlescan.containers.BluetoothLeDeviceStore; +import uk.co.alt236.btlescan.ui.common.Navigation; +import uk.co.alt236.btlescan.ui.common.recyclerview.RecyclerViewBinderCore; +import uk.co.alt236.btlescan.ui.common.recyclerview.RecyclerViewItem; +import uk.co.alt236.btlescan.ui.main.recyclerview.model.IBeaconItem; +import uk.co.alt236.btlescan.ui.main.recyclerview.model.LeDeviceItem; import uk.co.alt236.btlescan.util.BluetoothLeScanner; import uk.co.alt236.btlescan.util.BluetoothUtils; -import uk.co.alt236.easycursor.objectcursor.EasyObjectCursor; -public class MainActivity extends AppCompatActivity implements AdapterView.OnItemClickListener { +public class MainActivity extends AppCompatActivity { @Bind(R.id.tvBluetoothLe) protected TextView mTvBluetoothLeStatus; @Bind(R.id.tvBluetoothStatus) @@ -35,14 +44,15 @@ public class MainActivity extends AppCompatActivity implements AdapterView.OnIte @Bind(R.id.tvItemCount) protected TextView mTvItemCount; @Bind(android.R.id.list) - protected ListView mList; + protected RecyclerView mList; @Bind(android.R.id.empty) protected View mEmpty; + private RecyclerViewBinderCore mCore; private BluetoothUtils mBluetoothUtils; private BluetoothLeScanner mScanner; - private LeDeviceListAdapter mLeDeviceListAdapter; private BluetoothLeDeviceStore mDeviceStore; + private DeviceRecyclerAdapter mRecyclerAdapter; private final BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() { @Override @@ -50,51 +60,33 @@ public class MainActivity extends AppCompatActivity implements AdapterView.OnIte final BluetoothLeDevice deviceLe = new BluetoothLeDevice(device, rssi, scanRecord, System.currentTimeMillis()); mDeviceStore.addDevice(deviceLe); - final EasyObjectCursor c = mDeviceStore.getDeviceCursor(); + final List itemList = new ArrayList<>(); + + for (final BluetoothLeDevice leDevice : mDeviceStore.getDeviceList()) { + if (BeaconUtils.getBeaconType(leDevice) == BeaconType.IBEACON) { + itemList.add(new IBeaconItem(new IBeaconDevice(leDevice))); + } else { + itemList.add(new LeDeviceItem(leDevice)); + } + } runOnUiThread(new Runnable() { @Override public void run() { - mLeDeviceListAdapter.swapCursor(c); - updateItemCount(mLeDeviceListAdapter.getCount()); + mRecyclerAdapter.setData(itemList); + updateItemCount(mRecyclerAdapter.getItemCount()); } }); } }; - private void displayAboutDialog() { - // REALLY REALLY LAZY LINKIFIED DIALOG - final int paddingSizeDp = 5; - final float scale = getResources().getDisplayMetrics().density; - final int dpAsPixels = (int) (paddingSizeDp * scale + 0.5f); - - final TextView textView = new TextView(this); - final SpannableString text = new SpannableString(getString(R.string.about_dialog_text)); - - textView.setText(text); - textView.setAutoLinkMask(RESULT_OK); - textView.setMovementMethod(LinkMovementMethod.getInstance()); - textView.setPadding(dpAsPixels, dpAsPixels, dpAsPixels, dpAsPixels); - - Linkify.addLinks(text, Linkify.ALL); - new AlertDialog.Builder(this) - .setTitle(R.string.menu_about) - .setCancelable(false) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - public void onClick(final DialogInterface dialog, final int id) { - } - }) - .setView(textView) - .show(); - } - @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); - mList.setEmptyView(mEmpty); - mList.setOnItemClickListener(this); + mCore = RecyclerViewCoreFactory.create(this, new Navigation(this)); + mList.setLayoutManager(new LinearLayoutManager(this)); mDeviceStore = new BluetoothLeDeviceStore(); mBluetoothUtils = new BluetoothUtils(this); mScanner = new BluetoothLeScanner(mLeScanCallback, mBluetoothUtils); @@ -114,7 +106,7 @@ public class MainActivity extends AppCompatActivity implements AdapterView.OnIte menu.findItem(R.id.menu_refresh).setActionView(R.layout.actionbar_progress_indeterminate); } - if (mList.getCount() > 0) { + if (mRecyclerAdapter != null && mRecyclerAdapter.getItemCount() > 0) { menu.findItem(R.id.menu_share).setVisible(true); } else { menu.findItem(R.id.menu_share).setVisible(false); @@ -123,32 +115,36 @@ public class MainActivity extends AppCompatActivity implements AdapterView.OnIte return true; } - @Override - public void onItemClick(AdapterView adapterView, View view, int position, long l) { - final BluetoothLeDevice device = mLeDeviceListAdapter.getItem(position); - if (device == null) return; - - final Intent intent = new Intent(this, DeviceDetailsActivity.class); - intent.putExtra(DeviceDetailsActivity.EXTRA_DEVICE, device); - - startActivity(intent); - } - @Override public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.menu_scan: - startScan(); + PermissionsManager.getInstance().requestPermissionsIfNecessaryForResult(this, + new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, new PermissionsResultAction() { + + @Override + public void onGranted() { + startScan(); + } + + @Override + public void onDenied(String permission) { + Toast.makeText(MainActivity.this, + R.string.permission_not_granted_coarse_location, + Toast.LENGTH_SHORT) + .show(); + } + }); break; case R.id.menu_stop: mScanner.scanLeDevice(-1, false); invalidateOptionsMenu(); break; case R.id.menu_about: - displayAboutDialog(); + DialogFactory.createAboutDialog(this).show(); break; case R.id.menu_share: - mDeviceStore.shareDataAsEmail(this); + new Sharer().shareDataAsEmail(this, mDeviceStore); } return true; } @@ -162,16 +158,14 @@ public class MainActivity extends AppCompatActivity implements AdapterView.OnIte @Override public void onResume() { super.onResume(); - final boolean mIsBluetoothOn = mBluetoothUtils.isBluetoothOn(); - final boolean mIsBluetoothLePresent = mBluetoothUtils.isBluetoothLeSupported(); - if (mIsBluetoothOn) { + if (mBluetoothUtils.isBluetoothOn()) { mTvBluetoothStatus.setText(R.string.on); } else { mTvBluetoothStatus.setText(R.string.off); } - if (mIsBluetoothLePresent) { + if (mBluetoothUtils.isBluetoothLeSupported()) { mTvBluetoothLeStatus.setText(R.string.supported); } else { mTvBluetoothLeStatus.setText(R.string.not_supported); @@ -186,8 +180,8 @@ public class MainActivity extends AppCompatActivity implements AdapterView.OnIte mDeviceStore.clear(); updateItemCount(0); - mLeDeviceListAdapter = new LeDeviceListAdapter(this, mDeviceStore.getDeviceCursor()); - mList.setAdapter(mLeDeviceListAdapter); + mRecyclerAdapter = new DeviceRecyclerAdapter(mCore); + mList.setAdapter(mRecyclerAdapter); mBluetoothUtils.askUserToEnableBluetoothIfNeeded(); if (mIsBluetoothOn && mIsBluetoothLePresent) { @@ -203,4 +197,10 @@ public class MainActivity extends AppCompatActivity implements AdapterView.OnIte String.valueOf(count))); } + @Override + public void onRequestPermissionsResult(int requestCode, + @NonNull String[] permissions, + @NonNull int[] grantResults) { + PermissionsManager.getInstance().notifyPermissionsChange(permissions, grantResults); + } } diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/RecyclerViewCoreFactory.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/RecyclerViewCoreFactory.java new file mode 100644 index 0000000..dc6fe60 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/RecyclerViewCoreFactory.java @@ -0,0 +1,24 @@ +package uk.co.alt236.btlescan.ui.main; + +import android.content.Context; + +import uk.co.alt236.btlescan.R; +import uk.co.alt236.btlescan.ui.common.Navigation; +import uk.co.alt236.btlescan.ui.common.recyclerview.RecyclerViewBinderCore; +import uk.co.alt236.btlescan.ui.main.recyclerview.binder.IBeaconBinder; +import uk.co.alt236.btlescan.ui.main.recyclerview.binder.LeDeviceBinder; +import uk.co.alt236.btlescan.ui.main.recyclerview.holder.IBeaconHolder; +import uk.co.alt236.btlescan.ui.main.recyclerview.holder.LeDeviceHolder; + +/*protected*/ final class RecyclerViewCoreFactory { + + public static RecyclerViewBinderCore create(final Context context, final Navigation navigation) { + final RecyclerViewBinderCore core = new RecyclerViewBinderCore(); + + core.add(new IBeaconBinder(context, navigation), IBeaconHolder.class, R.layout.list_item_device_ibeacon); + core.add(new LeDeviceBinder(context, navigation), LeDeviceHolder.class, R.layout.list_item_device_le); + + return core; + } + +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/Sharer.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/Sharer.java new file mode 100644 index 0000000..0062c29 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/Sharer.java @@ -0,0 +1,161 @@ +package uk.co.alt236.btlescan.ui.main; + +import android.app.Activity; +import android.content.Context; +import android.net.Uri; +import android.support.v4.content.ContextCompat; +import android.widget.Toast; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.List; +import java.util.Locale; + +import uk.co.alt236.bluetoothlelib.device.BluetoothLeDevice; +import uk.co.alt236.bluetoothlelib.device.beacon.BeaconType; +import uk.co.alt236.bluetoothlelib.device.beacon.BeaconUtils; +import uk.co.alt236.bluetoothlelib.device.beacon.ibeacon.IBeaconDevice; +import uk.co.alt236.bluetoothlelib.util.ByteUtils; +import uk.co.alt236.btlescan.R; +import uk.co.alt236.btlescan.containers.BluetoothLeDeviceStore; +import uk.co.alt236.btlescan.ui.common.Navigation; +import uk.co.alt236.btlescan.util.TimeFormatter; + +/*package*/ class Sharer { + private static final String CSV_FILENAME_PREFIX = "bluetooth_le_%d"; + private static final String CSV_FILENAME_SUFFIX = ".csv"; + + private static File getExternalCacheDir(final Context context) { + final File[] files = ContextCompat.getExternalCacheDirs(context); + final File retVal; + + if (files == null || files.length == 0 || files[0] == null) { + retVal = null; + } else { + retVal = files[0]; + } + + return retVal; + } + + private static String getListAsCsv(List deviceList) { + + final StringBuilder sb = new StringBuilder(); + sb.append(CsvWriterHelper.addStuff("mac")); + sb.append(CsvWriterHelper.addStuff("name")); + sb.append(CsvWriterHelper.addStuff("firstTimestamp")); + sb.append(CsvWriterHelper.addStuff("firstRssi")); + sb.append(CsvWriterHelper.addStuff("currentTimestamp")); + sb.append(CsvWriterHelper.addStuff("currentRssi")); + sb.append(CsvWriterHelper.addStuff("adRecord")); + sb.append(CsvWriterHelper.addStuff("iBeacon")); + sb.append(CsvWriterHelper.addStuff("uuid")); + sb.append(CsvWriterHelper.addStuff("major")); + sb.append(CsvWriterHelper.addStuff("minor")); + sb.append(CsvWriterHelper.addStuff("txPower")); + sb.append(CsvWriterHelper.addStuff("distance")); + sb.append(CsvWriterHelper.addStuff("accuracy")); + sb.append('\n'); + + for (final BluetoothLeDevice device : deviceList) { + sb.append(CsvWriterHelper.addStuff(device.getAddress())); + sb.append(CsvWriterHelper.addStuff(device.getName())); + sb.append(CsvWriterHelper.addStuff(TimeFormatter.getIsoDateTime(device.getFirstTimestamp()))); + sb.append(CsvWriterHelper.addStuff(device.getFirstRssi())); + sb.append(CsvWriterHelper.addStuff(TimeFormatter.getIsoDateTime(device.getTimestamp()))); + sb.append(CsvWriterHelper.addStuff(device.getRssi())); + sb.append(CsvWriterHelper.addStuff(ByteUtils.byteArrayToHexString(device.getScanRecord()))); + final boolean isIBeacon = BeaconUtils.getBeaconType(device) == BeaconType.IBEACON; + final String uuid; + final String minor; + final String major; + final String txPower; + final String distance; + final String accuracy; + + if (isIBeacon) { + final IBeaconDevice beacon = new IBeaconDevice(device); + uuid = String.valueOf(beacon.getUUID()); + minor = String.valueOf(beacon.getMinor()); + major = String.valueOf(beacon.getMajor()); + txPower = String.valueOf(beacon.getCalibratedTxPower()); + distance = beacon.getDistanceDescriptor().toString().toLowerCase(Locale.US); + accuracy = String.valueOf(beacon.getAccuracy()); + } else { + uuid = ""; + minor = ""; + major = ""; + txPower = ""; + distance = ""; + accuracy = ""; + } + + sb.append(CsvWriterHelper.addStuff(isIBeacon)); + sb.append(CsvWriterHelper.addStuff(uuid)); + sb.append(CsvWriterHelper.addStuff(minor)); + sb.append(CsvWriterHelper.addStuff(major)); + sb.append(CsvWriterHelper.addStuff(txPower)); + sb.append(CsvWriterHelper.addStuff(distance)); + sb.append(CsvWriterHelper.addStuff(accuracy)); + + sb.append('\n'); + } + + return sb.toString(); + } + + private static FileWriter saveToFile(final File file, final String contents) { + FileWriter writer = null; + try { + writer = new FileWriter(file); + writer.append(contents); + writer.flush(); + + } catch (final IOException e) { + e.printStackTrace(); + } finally { + try { + if (writer != null) { + writer.close(); + } + } catch (final IOException e) { + e.printStackTrace(); + } + } + return writer; + } + + public void shareDataAsEmail(final Activity activity, + final BluetoothLeDeviceStore store) { + final long timeInMillis = System.currentTimeMillis(); + final String filename = String.format(Locale.US, CSV_FILENAME_PREFIX, timeInMillis); + + final String to = null; + final String subject = activity.getString( + R.string.exporter_email_device_list_subject, + TimeFormatter.getIsoDateTime(timeInMillis)); + + final String message = activity.getString(R.string.exporter_email_device_list_body); + + final String contents = getListAsCsv(store.getDeviceList()); + final File outputDir = getExternalCacheDir(activity); + + if (outputDir == null) { + Toast.makeText(activity, R.string.error_unable_to_access_external_storage, Toast.LENGTH_SHORT).show(); + } else { + try { + + final File outputFile = File.createTempFile(filename, CSV_FILENAME_SUFFIX, outputDir); + saveToFile(outputFile, contents); + + final Uri uri = Uri.fromFile(outputFile); + new Navigation(activity) + .shareFileViaEmail(uri, new String[]{to}, subject, message); + + } catch (final IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/recyclerview/binder/CommonBinding.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/recyclerview/binder/CommonBinding.java new file mode 100644 index 0000000..2a31192 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/recyclerview/binder/CommonBinding.java @@ -0,0 +1,36 @@ +package uk.co.alt236.btlescan.ui.main.recyclerview.binder; + +import android.content.Context; + +import uk.co.alt236.bluetoothlelib.device.BluetoothLeDevice; +import uk.co.alt236.btlescan.R; +import uk.co.alt236.btlescan.ui.main.recyclerview.holder.CommonDeviceHolder; +import uk.co.alt236.btlescan.util.Constants; + +/*package*/ class CommonBinding { + + public static void bind(final Context context, + final CommonDeviceHolder holder, + final BluetoothLeDevice device) { + + final String deviceName = device.getName(); + final double rssi = device.getRssi(); + + if (deviceName != null && deviceName.length() > 0) { + holder.getDeviceName().setText(deviceName); + } else { + holder.getDeviceName().setText(R.string.unknown_device); + } + + final String rssiString = + context.getString(R.string.formatter_db, String.valueOf(rssi)); + final String runningAverageRssiString = + context.getString(R.string.formatter_db, String.valueOf(device.getRunningAverageRssi())); + + holder.getDeviceLastUpdated().setText( + android.text.format.DateFormat.format( + Constants.TIME_FORMAT, new java.util.Date(device.getTimestamp()))); + holder.getDeviceAddress().setText(device.getAddress()); + holder.getDeviceRssi().setText(rssiString + " / " + runningAverageRssiString); + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/recyclerview/binder/IBeaconBinder.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/recyclerview/binder/IBeaconBinder.java new file mode 100644 index 0000000..9ccfb7f --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/recyclerview/binder/IBeaconBinder.java @@ -0,0 +1,54 @@ +package uk.co.alt236.btlescan.ui.main.recyclerview.binder; + +import android.content.Context; +import android.view.View; + +import uk.co.alt236.bluetoothlelib.device.beacon.ibeacon.IBeaconDevice; +import uk.co.alt236.btlescan.R; +import uk.co.alt236.btlescan.ui.common.Navigation; +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseViewBinder; +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseViewHolder; +import uk.co.alt236.btlescan.ui.common.recyclerview.RecyclerViewItem; +import uk.co.alt236.btlescan.ui.main.recyclerview.holder.IBeaconHolder; +import uk.co.alt236.btlescan.ui.main.recyclerview.model.IBeaconItem; +import uk.co.alt236.btlescan.util.Constants; + +public class IBeaconBinder extends BaseViewBinder { + + private final Navigation navigation; + + public IBeaconBinder(Context context, Navigation navigation) { + super(context); + this.navigation = navigation; + } + + @Override + public void bind(BaseViewHolder holder, IBeaconItem item) { + + final IBeaconHolder actualHolder = (IBeaconHolder) holder; + final IBeaconDevice device = item.getDevice(); + + final String accuracy = Constants.DOUBLE_TWO_DIGIT_ACCURACY.format(device.getAccuracy()); + + actualHolder.getIbeaconMajor().setText(String.valueOf(device.getMajor())); + actualHolder.getIbeaconMinor().setText(String.valueOf(device.getMinor())); + actualHolder.getIbeaconTxPower().setText(String.valueOf(device.getCalibratedTxPower())); + actualHolder.getIbeaconUUID().setText(device.getUUID()); + actualHolder.getIbeaconDistance().setText( + getContext().getString(R.string.formatter_meters, accuracy)); + actualHolder.getIbeaconDistanceDescriptor().setText(device.getDistanceDescriptor().toString()); + + CommonBinding.bind(getContext(), actualHolder, device); + actualHolder.getView().setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + navigation.openDetailsActivity(device); + } + }); + } + + @Override + public boolean canBind(RecyclerViewItem item) { + return item instanceof IBeaconItem; + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/recyclerview/binder/LeDeviceBinder.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/recyclerview/binder/LeDeviceBinder.java new file mode 100644 index 0000000..e6018b0 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/recyclerview/binder/LeDeviceBinder.java @@ -0,0 +1,42 @@ +package uk.co.alt236.btlescan.ui.main.recyclerview.binder; + +import android.content.Context; +import android.view.View; + +import uk.co.alt236.bluetoothlelib.device.BluetoothLeDevice; +import uk.co.alt236.btlescan.ui.common.Navigation; +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseViewBinder; +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseViewHolder; +import uk.co.alt236.btlescan.ui.common.recyclerview.RecyclerViewItem; +import uk.co.alt236.btlescan.ui.main.recyclerview.holder.LeDeviceHolder; +import uk.co.alt236.btlescan.ui.main.recyclerview.model.LeDeviceItem; + +public class LeDeviceBinder extends BaseViewBinder { + + private final Navigation navigation; + + public LeDeviceBinder(Context context, Navigation navigation) { + super(context); + this.navigation = navigation; + } + + @Override + public void bind(BaseViewHolder holder, LeDeviceItem item) { + + final LeDeviceHolder actualHolder = (LeDeviceHolder) holder; + final BluetoothLeDevice device = item.getDevice(); + + CommonBinding.bind(getContext(), actualHolder, device); + actualHolder.getView().setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + navigation.openDetailsActivity(device); + } + }); + } + + @Override + public boolean canBind(RecyclerViewItem item) { + return item instanceof LeDeviceItem; + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/recyclerview/holder/CommonDeviceHolder.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/recyclerview/holder/CommonDeviceHolder.java new file mode 100644 index 0000000..eee330d --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/recyclerview/holder/CommonDeviceHolder.java @@ -0,0 +1,13 @@ +package uk.co.alt236.btlescan.ui.main.recyclerview.holder; + +import android.widget.TextView; + +public interface CommonDeviceHolder { + TextView getDeviceName(); + + TextView getDeviceAddress(); + + TextView getDeviceRssi(); + + TextView getDeviceLastUpdated(); +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/recyclerview/holder/IBeaconHolder.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/recyclerview/holder/IBeaconHolder.java new file mode 100644 index 0000000..9d69dc1 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/recyclerview/holder/IBeaconHolder.java @@ -0,0 +1,83 @@ +package uk.co.alt236.btlescan.ui.main.recyclerview.holder; + +import android.view.View; +import android.widget.TextView; + +import uk.co.alt236.btlescan.R; +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseViewHolder; +import uk.co.alt236.btlescan.ui.main.recyclerview.model.IBeaconItem; + +public class IBeaconHolder extends BaseViewHolder implements CommonDeviceHolder { + + private final TextView ibeaconUUID; + private final TextView ibeaconMajor; + private final TextView ibeaconMinor; + private final TextView ibeaconTxPower; + private final TextView ibeaconDistance; + private final TextView ibeaconDistanceDescriptor; + + private final TextView deviceName; + private final TextView deviceAddress; + private final TextView deviceRssi; + private final TextView deviceLastUpdated; + + public IBeaconHolder(View itemView) { + super(itemView); + + this.deviceAddress = (TextView) itemView.findViewById(R.id.device_address); + this.deviceName = (TextView) itemView.findViewById(R.id.device_name); + this.deviceRssi = (TextView) itemView.findViewById(R.id.device_rssi); + this.deviceLastUpdated = (TextView) itemView.findViewById(R.id.device_last_update); + + this.ibeaconMajor = (TextView) itemView.findViewById(R.id.ibeacon_major); + this.ibeaconMinor = (TextView) itemView.findViewById(R.id.ibeacon_minor); + this.ibeaconDistance = (TextView) itemView.findViewById(R.id.ibeacon_distance); + this.ibeaconUUID = (TextView) itemView.findViewById(R.id.ibeacon_uuid); + this.ibeaconTxPower = (TextView) itemView.findViewById(R.id.ibeacon_tx_power); + this.ibeaconDistanceDescriptor = (TextView) itemView.findViewById(R.id.ibeacon_distance_descriptor); + } + + @Override + public TextView getDeviceName() { + return deviceName; + } + + @Override + public TextView getDeviceAddress() { + return deviceAddress; + } + + @Override + public TextView getDeviceRssi() { + return deviceRssi; + } + + @Override + public TextView getDeviceLastUpdated() { + return deviceLastUpdated; + } + + public TextView getIbeaconUUID() { + return ibeaconUUID; + } + + public TextView getIbeaconMajor() { + return ibeaconMajor; + } + + public TextView getIbeaconMinor() { + return ibeaconMinor; + } + + public TextView getIbeaconTxPower() { + return ibeaconTxPower; + } + + public TextView getIbeaconDistance() { + return ibeaconDistance; + } + + public TextView getIbeaconDistanceDescriptor() { + return ibeaconDistanceDescriptor; + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/recyclerview/holder/LeDeviceHolder.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/recyclerview/holder/LeDeviceHolder.java new file mode 100644 index 0000000..793bd70 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/recyclerview/holder/LeDeviceHolder.java @@ -0,0 +1,45 @@ +package uk.co.alt236.btlescan.ui.main.recyclerview.holder; + +import android.view.View; +import android.widget.TextView; + +import uk.co.alt236.btlescan.R; +import uk.co.alt236.btlescan.ui.common.recyclerview.BaseViewHolder; +import uk.co.alt236.btlescan.ui.main.recyclerview.model.LeDeviceItem; + +public class LeDeviceHolder extends BaseViewHolder implements CommonDeviceHolder { + + private final TextView deviceName; + private final TextView deviceAddress; + private final TextView deviceRssi; + private final TextView deviceLastUpdated; + + public LeDeviceHolder(View itemView) { + super(itemView); + + this.deviceAddress = (TextView) itemView.findViewById(R.id.device_address); + this.deviceName = (TextView) itemView.findViewById(R.id.device_name); + this.deviceRssi = (TextView) itemView.findViewById(R.id.device_rssi); + this.deviceLastUpdated = (TextView) itemView.findViewById(R.id.device_last_update); + } + + @Override + public TextView getDeviceName() { + return deviceName; + } + + @Override + public TextView getDeviceAddress() { + return deviceAddress; + } + + @Override + public TextView getDeviceRssi() { + return deviceRssi; + } + + @Override + public TextView getDeviceLastUpdated() { + return deviceLastUpdated; + } +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/recyclerview/model/IBeaconItem.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/recyclerview/model/IBeaconItem.java new file mode 100644 index 0000000..1c7c802 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/recyclerview/model/IBeaconItem.java @@ -0,0 +1,18 @@ +package uk.co.alt236.btlescan.ui.main.recyclerview.model; + +import uk.co.alt236.bluetoothlelib.device.beacon.ibeacon.IBeaconDevice; +import uk.co.alt236.btlescan.ui.common.recyclerview.RecyclerViewItem; + +public class IBeaconItem implements RecyclerViewItem { + + private final IBeaconDevice device; + + public IBeaconItem(final IBeaconDevice device) { + this.device = device; + } + + public IBeaconDevice getDevice() { + return device; + } + +} diff --git a/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/recyclerview/model/LeDeviceItem.java b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/recyclerview/model/LeDeviceItem.java new file mode 100644 index 0000000..77ac046 --- /dev/null +++ b/sample_app/src/main/java/uk/co/alt236/btlescan/ui/main/recyclerview/model/LeDeviceItem.java @@ -0,0 +1,17 @@ +package uk.co.alt236.btlescan.ui.main.recyclerview.model; + +import uk.co.alt236.bluetoothlelib.device.BluetoothLeDevice; +import uk.co.alt236.btlescan.ui.common.recyclerview.RecyclerViewItem; + +public class LeDeviceItem implements RecyclerViewItem { + + private final BluetoothLeDevice device; + + public LeDeviceItem(final BluetoothLeDevice device) { + this.device = device; + } + + public BluetoothLeDevice getDevice() { + return device; + } +} diff --git a/sample_app/src/main/res/drawable-hdpi/ic_action_share.png b/sample_app/src/main/res/drawable-hdpi/ic_action_share.png index 2b267df..8385807 100644 Binary files a/sample_app/src/main/res/drawable-hdpi/ic_action_share.png and b/sample_app/src/main/res/drawable-hdpi/ic_action_share.png differ diff --git a/sample_app/src/main/res/drawable-hdpi/ic_bluetooth.png b/sample_app/src/main/res/drawable-hdpi/ic_bluetooth.png new file mode 100644 index 0000000..af26e93 Binary files /dev/null and b/sample_app/src/main/res/drawable-hdpi/ic_bluetooth.png differ diff --git a/sample_app/src/main/res/drawable-hdpi/ic_launcher.png b/sample_app/src/main/res/drawable-hdpi/ic_launcher.png deleted file mode 100644 index 1563c4d..0000000 Binary files a/sample_app/src/main/res/drawable-hdpi/ic_launcher.png and /dev/null differ diff --git a/sample_app/src/main/res/drawable-mdpi/ic_action_share.png b/sample_app/src/main/res/drawable-mdpi/ic_action_share.png index 5dba2eb..8442099 100644 Binary files a/sample_app/src/main/res/drawable-mdpi/ic_action_share.png and b/sample_app/src/main/res/drawable-mdpi/ic_action_share.png differ diff --git a/sample_app/src/main/res/drawable-mdpi/ic_bluetooth.png b/sample_app/src/main/res/drawable-mdpi/ic_bluetooth.png new file mode 100644 index 0000000..6734d1d Binary files /dev/null and b/sample_app/src/main/res/drawable-mdpi/ic_bluetooth.png differ diff --git a/sample_app/src/main/res/drawable-mdpi/ic_launcher.png b/sample_app/src/main/res/drawable-mdpi/ic_launcher.png deleted file mode 100644 index de3905b..0000000 Binary files a/sample_app/src/main/res/drawable-mdpi/ic_launcher.png and /dev/null differ diff --git a/sample_app/src/main/res/drawable-xhdpi/ic_action_share.png b/sample_app/src/main/res/drawable-xhdpi/ic_action_share.png index 0ae218e..c7e0767 100644 Binary files a/sample_app/src/main/res/drawable-xhdpi/ic_action_share.png and b/sample_app/src/main/res/drawable-xhdpi/ic_action_share.png differ diff --git a/sample_app/src/main/res/drawable-xhdpi/ic_bluetooth.png b/sample_app/src/main/res/drawable-xhdpi/ic_bluetooth.png index 2434af2..7c6f270 100644 Binary files a/sample_app/src/main/res/drawable-xhdpi/ic_bluetooth.png and b/sample_app/src/main/res/drawable-xhdpi/ic_bluetooth.png differ diff --git a/sample_app/src/main/res/drawable-xhdpi/ic_launcher.png b/sample_app/src/main/res/drawable-xhdpi/ic_launcher.png deleted file mode 100644 index d737af2..0000000 Binary files a/sample_app/src/main/res/drawable-xhdpi/ic_launcher.png and /dev/null differ diff --git a/sample_app/src/main/res/drawable-xxhdpi/ic_action_share.png b/sample_app/src/main/res/drawable-xxhdpi/ic_action_share.png index ea9a8be..21ee244 100644 Binary files a/sample_app/src/main/res/drawable-xxhdpi/ic_action_share.png and b/sample_app/src/main/res/drawable-xxhdpi/ic_action_share.png differ diff --git a/sample_app/src/main/res/drawable-xxhdpi/ic_bluetooth.png b/sample_app/src/main/res/drawable-xxhdpi/ic_bluetooth.png new file mode 100644 index 0000000..2303076 Binary files /dev/null and b/sample_app/src/main/res/drawable-xxhdpi/ic_bluetooth.png differ diff --git a/sample_app/src/main/res/drawable-xxhdpi/ic_launcher.png b/sample_app/src/main/res/drawable-xxhdpi/ic_launcher.png deleted file mode 100644 index 48cc2da..0000000 Binary files a/sample_app/src/main/res/drawable-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/sample_app/src/main/res/drawable-xxxhdpi/ic_action_share.png b/sample_app/src/main/res/drawable-xxxhdpi/ic_action_share.png new file mode 100644 index 0000000..62dddfa Binary files /dev/null and b/sample_app/src/main/res/drawable-xxxhdpi/ic_action_share.png differ diff --git a/sample_app/src/main/res/drawable-xxxhdpi/ic_bluetooth.png b/sample_app/src/main/res/drawable-xxxhdpi/ic_bluetooth.png new file mode 100644 index 0000000..5bcfc4c Binary files /dev/null and b/sample_app/src/main/res/drawable-xxxhdpi/ic_bluetooth.png differ diff --git a/sample_app/src/main/res/layout/activity_details.xml b/sample_app/src/main/res/layout/activity_details.xml index 3100292..56ddbc9 100644 --- a/sample_app/src/main/res/layout/activity_details.xml +++ b/sample_app/src/main/res/layout/activity_details.xml @@ -6,9 +6,9 @@ android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin"> - - + + \ No newline at end of file diff --git a/sample_app/src/main/res/layout/activity_gatt_services.xml b/sample_app/src/main/res/layout/activity_gatt_services.xml index af806fb..15d2827 100644 --- a/sample_app/src/main/res/layout/activity_gatt_services.xml +++ b/sample_app/src/main/res/layout/activity_gatt_services.xml @@ -57,7 +57,7 @@ android:layout_width="match_parent" android:layout_height="1dp" android:layout_below="@id/deviceInformation" - android:background="@android:color/holo_blue_dark"/> + android:background="@color/colorSeparator" /> + android:background="@color/colorSeparator" /> + tools:context=".ui.main.MainActivity"> + android:background="@color/colorSeparator" /> + android:background="@color/colorSeparator" /> - diff --git a/sample_app/src/main/res/layout/dialog_textview.xml b/sample_app/src/main/res/layout/dialog_textview.xml new file mode 100644 index 0000000..0ad79c2 --- /dev/null +++ b/sample_app/src/main/res/layout/dialog_textview.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/sample_app/src/main/res/layout/list_item_device.xml b/sample_app/src/main/res/layout/list_item_device_ibeacon.xml similarity index 69% rename from sample_app/src/main/res/layout/list_item_device.xml rename to sample_app/src/main/res/layout/list_item_device_ibeacon.xml index 4a2bc2d..d0b9ca9 100644 --- a/sample_app/src/main/res/layout/list_item_device.xml +++ b/sample_app/src/main/res/layout/list_item_device_ibeacon.xml @@ -1,5 +1,4 @@ - - - - - diff --git a/sample_app/src/main/res/values-v14/styles.xml b/sample_app/src/main/res/values-v14/styles.xml deleted file mode 100644 index 664f4f1..0000000 --- a/sample_app/src/main/res/values-v14/styles.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/sample_app/src/main/res/values-v21/styles.xml b/sample_app/src/main/res/values-v21/styles.xml new file mode 100644 index 0000000..79edc9c --- /dev/null +++ b/sample_app/src/main/res/values-v21/styles.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/sample_app/src/main/res/values/colors.xml b/sample_app/src/main/res/values/colors.xml index c64f146..8399d14 100644 --- a/sample_app/src/main/res/values/colors.xml +++ b/sample_app/src/main/res/values/colors.xml @@ -1,3 +1,10 @@ + #0082FC #66e0e0e0 + + @color/bluetooth_blue + #0069E3 + #F06292 + + @color/colorAccent diff --git a/sample_app/src/main/res/values/dimens.xml b/sample_app/src/main/res/values/dimens.xml index 55c1e59..d72218f 100644 --- a/sample_app/src/main/res/values/dimens.xml +++ b/sample_app/src/main/res/values/dimens.xml @@ -4,4 +4,5 @@ 16dp 16dp + 8dp diff --git a/sample_app/src/main/res/values/strings.xml b/sample_app/src/main/res/values/strings.xml index 84c3f4f..a6b422f 100644 --- a/sample_app/src/main/res/values/strings.xml +++ b/sample_app/src/main/res/values/strings.xml @@ -5,6 +5,7 @@ Bluetooth LE Scanner Connected Disconnected + Connecting Invalid Device Data! No data Not supported @@ -21,6 +22,7 @@ %sm %sdb Items: %s + \'%s\' About @@ -72,4 +74,7 @@ UUID: Updated: Descriptor: + + "Could not access external storage!" + The ACCESS_COARSE_LOCATION permission is needed to receive bluetooth scan results \ No newline at end of file diff --git a/sample_app/src/main/res/values/styles.xml b/sample_app/src/main/res/values/styles.xml index c503b3f..c3f501b 100644 --- a/sample_app/src/main/res/values/styles.xml +++ b/sample_app/src/main/res/values/styles.xml @@ -1,20 +1,11 @@ - - +