BLE 入门 -> NullPointerException
Getting started with BLE -> NullPointerException
我对本书 "Getting Started with Bluetooth Low Energy" 中的教程有疑问。
我只想扫描 BLE 设备并通过 logcat post 结果。但只要我开始扫描,应用程序就会关闭并给我一个 NullPointerException(参见下面的 Logcat)。对于这个例子,我使用的是 BleWrapper Class,但我也尝试在另一个应用程序中设置没有它的 BLE 扫描。但它总是同样的错误...... NullPointerException ......
我还尝试了 2 个不同的演示应用程序,将它们导入我的 Android 工作室(v1.1)并且它们表现良好。但是,如果我尝试在其自己的 activity 中编写相同的代码,它将因此错误而崩溃。
我猜是回调或扫描方法有问题,也许我忘记了一些参考?
这是我的主要内容 activity:
package com.example.oliver.blebuch;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Intent;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
public class MainActivity extends ActionBarActivity {
private BleWrapper mBleWrapper = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d("tag", "Test");
mBleWrapper = new BleWrapper(this, new BleWrapperUiCallbacks.Null()
{
@Override
public void uiDeviceFound(final BluetoothDevice device,
final int rssi,
final byte[] scanRecord)
{
Log.d("tag", "uiDeviceFound: "+device.getName()+", "+rssi+", "+scanRecord.toString());
}
});
if(mBleWrapper.checkBleHardwareAvailable() == false)
{
Toast.makeText(this,"No BLE-compatible hardware detected",
Toast.LENGTH_SHORT).show();
finish();
}
}
protected void onResume(){
super.onResume();
//check for Bluetooth enabled on each resume
if (mBleWrapper.isBtEnabled() == false)
{
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivity(enableBtIntent);
finish();
}
}
@Override
protected void onPause(){
super.onPause();
mBleWrapper.disconnect();
mBleWrapper.close();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId())
{
case R.id.action_scan:
mBleWrapper.startScanning();
break;
case R.id.action_stop:
mBleWrapper.stopScanning();
break;
default:
break;
}
return super.onOptionsItemSelected(item);
}
}
Logcat:
03-26 10:28:49.986 26288-26288/com.example.oliver.blebuch D/OpenGLRenderer﹕ Enabling debug mode 0
03-26 10:28:53.236 26288-26288/com.example.oliver.blebuch W/dalvikvm﹕ method Landroid/support/v7/internal/widget/ListViewCompat;.lookForSelectablePosition incorrectly overrides package-private method with same name in Landroid/widget/ListView;
03-26 10:28:53.266 26288-26288/com.example.oliver.blebuch D/AbsListView﹕ Get MotionRecognitionManager
03-26 10:28:54.366 26288-26288/com.example.oliver.blebuch D/AndroidRuntime﹕ Shutting down VM
03-26 10:28:54.366 26288-26288/com.example.oliver.blebuch W/dalvikvm﹕ threadid=1: thread exiting with uncaught exception (group=0x41783da0)
03-26 10:28:54.376 26288-26288/com.example.oliver.blebuch E/AndroidRuntime﹕ FATAL EXCEPTION: main
Process: com.example.oliver.blebuch, PID: 26288
java.lang.NullPointerException
at com.example.oliver.blebuch.BleWrapper.startScanning(BleWrapper.java:79)
at com.example.oliver.blebuch.MainActivity.onOptionsItemSelected(MainActivity.java:78)
at android.app.Activity.onMenuItemSelected(Activity.java:2708)
at android.support.v4.app.FragmentActivity.onMenuItemSelected(FragmentActivity.java:350)
at android.support.v7.app.ActionBarActivity.onMenuItemSelected(ActionBarActivity.java:155)
at android.support.v7.app.ActionBarActivityDelegate.onMenuItemSelected(ActionBarActivityDelegate.java:74)
at android.support.v7.app.ActionBarActivityDelegateBase.onMenuItemSelected(ActionBarActivityDelegateBase.java:556)
at android.support.v7.internal.view.menu.MenuBuilder.dispatchMenuItemSelected(MenuBuilder.java:802)
at android.support.v7.internal.view.menu.MenuItemImpl.invoke(MenuItemImpl.java:153)
at android.support.v7.internal.view.menu.MenuBuilder.performItemAction(MenuBuilder.java:949)
at android.support.v7.internal.view.menu.MenuBuilder.performItemAction(MenuBuilder.java:939)
at android.support.v7.internal.view.menu.MenuPopupHelper.onItemClick(MenuPopupHelper.java:187)
at android.widget.AdapterView.performItemClick(AdapterView.java:308)
at android.widget.AbsListView.performItemClick(AbsListView.java:1495)
at android.widget.AbsListView$PerformClick.run(AbsListView.java:3453)
at android.widget.AbsListView.run(AbsListView.java:4816)
at android.os.Handler.handleCallback(Handler.java:733)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:136)
at android.app.ActivityThread.main(ActivityThread.java:5479)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1283)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1099)
at dalvik.system.NativeStart.main(Native Method)
BLE 包装器 Class:
package com.example.oliver.blebuch;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.util.Log;
public class BleWrapper {
/* defines (in milliseconds) how often RSSI should be updated */
private static final int RSSI_UPDATE_TIME_INTERVAL = 1500; // 1.5 seconds
/* callback object through which we are returning results to the caller */
private BleWrapperUiCallbacks mUiCallback = null;
/* define NULL object for UI callbacks */
private static final BleWrapperUiCallbacks NULL_CALLBACK = new BleWrapperUiCallbacks.Null();
/* creates BleWrapper object, set its parent activity and callback object */
public BleWrapper(Activity parent, BleWrapperUiCallbacks callback) {
this.mParent = parent;
mUiCallback = callback;
if(mUiCallback == null) mUiCallback = NULL_CALLBACK;
}
public BluetoothManager getManager() { return mBluetoothManager; }
public BluetoothAdapter getAdapter() { return mBluetoothAdapter; }
public BluetoothDevice getDevice() { return mBluetoothDevice; }
public BluetoothGatt getGatt() { return mBluetoothGatt; }
public BluetoothGattService getCachedService() { return mBluetoothSelectedService; }
public List<BluetoothGattService> getCachedServices() { return mBluetoothGattServices; }
public boolean isConnected() { return mConnected; }
/* run test and check if this device has BT and BLE hardware available */
public boolean checkBleHardwareAvailable() {
// First check general Bluetooth Hardware:
// get BluetoothManager...
final BluetoothManager manager = (BluetoothManager) mParent.getSystemService(Context.BLUETOOTH_SERVICE);
if(manager == null) return false;
// .. and then get adapter from manager
final BluetoothAdapter adapter = manager.getAdapter();
if(adapter == null) return false;
// and then check if BT LE is also available
boolean hasBle = mParent.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);
return hasBle;
}
/* before any action check if BT is turned ON and enabled for us
* call this in onResume to be always sure that BT is ON when Your
* application is put into the foreground */
public boolean isBtEnabled() {
final BluetoothManager manager = (BluetoothManager) mParent.getSystemService(Context.BLUETOOTH_SERVICE);
if(manager == null) return false;
final BluetoothAdapter adapter = manager.getAdapter();
if(adapter == null) return false;
return adapter.isEnabled();
}
/* start scanning for BT LE devices around */
public void startScanning() {
mBluetoothAdapter.startLeScan(mDeviceFoundCallback);
}
/* stops current scanning */
public void stopScanning() {
mBluetoothAdapter.stopLeScan(mDeviceFoundCallback);
}
/* initialize BLE and get BT Manager & Adapter */
public boolean initialize() {
if (mBluetoothManager == null) {
mBluetoothManager = (BluetoothManager) mParent.getSystemService(Context.BLUETOOTH_SERVICE);
if (mBluetoothManager == null) {
return false;
}
}
if(mBluetoothAdapter == null) mBluetoothAdapter = mBluetoothManager.getAdapter();
if (mBluetoothAdapter == null) {
return false;
}
return true;
}
/* connect to the device with specified address */
public boolean connect(final String deviceAddress) {
if (mBluetoothAdapter == null || deviceAddress == null) return false;
mDeviceAddress = deviceAddress;
// check if we need to connect from scratch or just reconnect to previous device
if(mBluetoothGatt != null && mBluetoothGatt.getDevice().getAddress().equals(deviceAddress)) {
// just reconnect
return mBluetoothGatt.connect();
}
else {
// connect from scratch
// get BluetoothDevice object for specified address
mBluetoothDevice = mBluetoothAdapter.getRemoteDevice(mDeviceAddress);
if (mBluetoothDevice == null) {
// we got wrong address - that device is not available!
return false;
}
// connect with remote device
mBluetoothGatt = mBluetoothDevice.connectGatt(mParent, false, mBleCallback);
}
return true;
}
/* disconnect the device. It is still possible to reconnect to it later with this Gatt client */
public void disconnect() {
if(mBluetoothGatt != null) mBluetoothGatt.disconnect();
mUiCallback.uiDeviceDisconnected(mBluetoothGatt, mBluetoothDevice);
}
/* close GATT client completely */
public void close() {
if(mBluetoothGatt != null) mBluetoothGatt.close();
mBluetoothGatt = null;
}
/* request new RSSi value for the connection*/
public void readPeriodicalyRssiValue(final boolean repeat) {
mTimerEnabled = repeat;
// check if we should stop checking RSSI value
if(mConnected == false || mBluetoothGatt == null || mTimerEnabled == false) {
mTimerEnabled = false;
return;
}
mTimerHandler.postDelayed(new Runnable() {
@Override
public void run() {
if(mBluetoothGatt == null ||
mBluetoothAdapter == null ||
mConnected == false)
{
mTimerEnabled = false;
return;
}
// request RSSI value
mBluetoothGatt.readRemoteRssi();
// add call it once more in the future
readPeriodicalyRssiValue(mTimerEnabled);
}
}, RSSI_UPDATE_TIME_INTERVAL);
}
/* starts monitoring RSSI value */
public void startMonitoringRssiValue() {
readPeriodicalyRssiValue(true);
}
/* stops monitoring of RSSI value */
public void stopMonitoringRssiValue() {
readPeriodicalyRssiValue(false);
}
/* request to discover all services available on the remote devices
* results are delivered through callback object */
public void startServicesDiscovery() {
if(mBluetoothGatt != null) mBluetoothGatt.discoverServices();
}
/* gets services and calls UI callback to handle them
* before calling getServices() make sure service discovery is finished! */
public void getSupportedServices() {
if(mBluetoothGattServices != null && mBluetoothGattServices.size() > 0) mBluetoothGattServices.clear();
// keep reference to all services in local array:
if(mBluetoothGatt != null) mBluetoothGattServices = mBluetoothGatt.getServices();
mUiCallback.uiAvailableServices(mBluetoothGatt, mBluetoothDevice, mBluetoothGattServices);
}
/* get all characteristic for particular service and pass them to the UI callback */
public void getCharacteristicsForService(final BluetoothGattService service) {
if(service == null) return;
List<BluetoothGattCharacteristic> chars = null;
chars = service.getCharacteristics();
mUiCallback.uiCharacteristicForService(mBluetoothGatt, mBluetoothDevice, service, chars);
// keep reference to the last selected service
mBluetoothSelectedService = service;
}
/* request to fetch newest value stored on the remote device for particular characteristic */
public void requestCharacteristicValue(BluetoothGattCharacteristic ch) {
if (mBluetoothAdapter == null || mBluetoothGatt == null) return;
mBluetoothGatt.readCharacteristic(ch);
// new value available will be notified in Callback Object
}
/* get characteristic's value (and parse it for some types of characteristics)
* before calling this You should always update the value by calling requestCharacteristicValue() */
public void getCharacteristicValue(BluetoothGattCharacteristic ch) {
if (mBluetoothAdapter == null || mBluetoothGatt == null || ch == null) return;
byte[] rawValue = ch.getValue();
String strValue = null;
int intValue = 0;
// lets read and do real parsing of some characteristic to get meaningful value from it
UUID uuid = ch.getUuid();
if(uuid.equals(BleDefinedUUIDs.Characteristic.HEART_RATE_MEASUREMENT)) { // heart rate
// follow https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.heart_rate_measurement.xml
// first check format used by the device - it is specified in bit 0 and tells us if we should ask for index 1 (and uint8) or index 2 (and uint16)
int index = ((rawValue[0] & 0x01) == 1) ? 2 : 1;
// also we need to define format
int format = (index == 1) ? BluetoothGattCharacteristic.FORMAT_UINT8 : BluetoothGattCharacteristic.FORMAT_UINT16;
// now we have everything, get the value
intValue = ch.getIntValue(format, index);
strValue = intValue + " bpm"; // it is always in bpm units
}
else if (uuid.equals(BleDefinedUUIDs.Characteristic.HEART_RATE_MEASUREMENT) || // manufacturer name string
uuid.equals(BleDefinedUUIDs.Characteristic.MODEL_NUMBER_STRING) || // model number string)
uuid.equals(BleDefinedUUIDs.Characteristic.FIRMWARE_REVISION_STRING)) // firmware revision string
{
// follow https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.manufacturer_name_string.xml etc.
// string value are usually simple utf8s string at index 0
strValue = ch.getStringValue(0);
}
else if(uuid.equals(BleDefinedUUIDs.Characteristic.APPEARANCE)) { // appearance
// follow: https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.gap.appearance.xml
intValue = ((int)rawValue[1]) << 8;
intValue += rawValue[0];
strValue = BleNamesResolver.resolveAppearance(intValue);
}
else if(uuid.equals(BleDefinedUUIDs.Characteristic.BODY_SENSOR_LOCATION)) { // body sensor location
// follow: https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.body_sensor_location.xml
intValue = rawValue[0];
strValue = BleNamesResolver.resolveHeartRateSensorLocation(intValue);
}
else if(uuid.equals(BleDefinedUUIDs.Characteristic.BATTERY_LEVEL)) { // battery level
// follow: https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.battery_level.xml
intValue = rawValue[0];
strValue = "" + intValue + "% battery level";
}
else {
// not known type of characteristic, so we need to handle this in "general" way
// get first four bytes and transform it to integer
intValue = 0;
if(rawValue.length > 0) intValue = (int)rawValue[0];
if(rawValue.length > 1) intValue = intValue + ((int)rawValue[1] << 8);
if(rawValue.length > 2) intValue = intValue + ((int)rawValue[2] << 8);
if(rawValue.length > 3) intValue = intValue + ((int)rawValue[3] << 8);
if (rawValue.length > 0) {
final StringBuilder stringBuilder = new StringBuilder(rawValue.length);
for(byte byteChar : rawValue) {
stringBuilder.append(String.format("%c", byteChar));
}
strValue = stringBuilder.toString();
}
}
String timestamp = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss.SSS").format(new Date());
mUiCallback.uiNewValueForCharacteristic(mBluetoothGatt,
mBluetoothDevice,
mBluetoothSelectedService,
ch,
strValue,
intValue,
rawValue,
timestamp);
}
/* reads and return what what FORMAT is indicated by characteristic's properties
* seems that value makes no sense in most cases */
public int getValueFormat(BluetoothGattCharacteristic ch) {
int properties = ch.getProperties();
if((BluetoothGattCharacteristic.FORMAT_FLOAT & properties) != 0) return BluetoothGattCharacteristic.FORMAT_FLOAT;
if((BluetoothGattCharacteristic.FORMAT_SFLOAT & properties) != 0) return BluetoothGattCharacteristic.FORMAT_SFLOAT;
if((BluetoothGattCharacteristic.FORMAT_SINT16 & properties) != 0) return BluetoothGattCharacteristic.FORMAT_SINT16;
if((BluetoothGattCharacteristic.FORMAT_SINT32 & properties) != 0) return BluetoothGattCharacteristic.FORMAT_SINT32;
if((BluetoothGattCharacteristic.FORMAT_SINT8 & properties) != 0) return BluetoothGattCharacteristic.FORMAT_SINT8;
if((BluetoothGattCharacteristic.FORMAT_UINT16 & properties) != 0) return BluetoothGattCharacteristic.FORMAT_UINT16;
if((BluetoothGattCharacteristic.FORMAT_UINT32 & properties) != 0) return BluetoothGattCharacteristic.FORMAT_UINT32;
if((BluetoothGattCharacteristic.FORMAT_UINT8 & properties) != 0) return BluetoothGattCharacteristic.FORMAT_UINT8;
return 0;
}
/* set new value for particular characteristic */
public void writeDataToCharacteristic(final BluetoothGattCharacteristic ch, final byte[] dataToWrite) {
if (mBluetoothAdapter == null || mBluetoothGatt == null || ch == null) return;
// first set it locally....
ch.setValue(dataToWrite);
// ... and then "commit" changes to the peripheral
mBluetoothGatt.writeCharacteristic(ch);
}
/* enables/disables notification for characteristic */
public void setNotificationForCharacteristic(BluetoothGattCharacteristic ch, boolean enabled) {
if (mBluetoothAdapter == null || mBluetoothGatt == null) return;
boolean success = mBluetoothGatt.setCharacteristicNotification(ch, enabled);
if(!success) {
Log.e("------", "Seting proper notification status for characteristic failed!");
}
// This is also sometimes required (e.g. for heart rate monitors) to enable notifications/indications
// see: https://developer.bluetooth.org/gatt/descriptors/Pages/DescriptorViewer.aspx?u=org.bluetooth.descriptor.gatt.client_characteristic_configuration.xml
BluetoothGattDescriptor descriptor = ch.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
if(descriptor != null) {
byte[] val = enabled ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE;
descriptor.setValue(val);
mBluetoothGatt.writeDescriptor(descriptor);
}
}
/* defines callback for scanning results */
private BluetoothAdapter.LeScanCallback mDeviceFoundCallback = new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(final BluetoothDevice device, final int rssi, final byte[] scanRecord) {
mUiCallback.uiDeviceFound(device, rssi, scanRecord);
}
};
/* callbacks called for any action on particular Ble Device */
private final BluetoothGattCallback mBleCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
mConnected = true;
mUiCallback.uiDeviceConnected(mBluetoothGatt, mBluetoothDevice);
// now we can start talking with the device, e.g.
mBluetoothGatt.readRemoteRssi();
// response will be delivered to callback object!
// in our case we would also like automatically to call for services discovery
startServicesDiscovery();
// and we also want to get RSSI value to be updated periodically
startMonitoringRssiValue();
}
else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
mConnected = false;
mUiCallback.uiDeviceDisconnected(mBluetoothGatt, mBluetoothDevice);
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
// now, when services discovery is finished, we can call getServices() for Gatt
getSupportedServices();
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status)
{
// we got response regarding our request to fetch characteristic value
if (status == BluetoothGatt.GATT_SUCCESS) {
// and it success, so we can get the value
getCharacteristicValue(characteristic);
}
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic)
{
// characteristic's value was updated due to enabled notification, lets get this value
// the value itself will be reported to the UI inside getCharacteristicValue
getCharacteristicValue(characteristic);
// also, notify UI that notification are enabled for particular characteristic
mUiCallback.uiGotNotification(mBluetoothGatt, mBluetoothDevice, mBluetoothSelectedService, characteristic);
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
String deviceName = gatt.getDevice().getName();
String serviceName = BleNamesResolver.resolveServiceName(characteristic.getService().getUuid().toString().toLowerCase(Locale.getDefault()));
String charName = BleNamesResolver.resolveCharacteristicName(characteristic.getUuid().toString().toLowerCase(Locale.getDefault()));
String description = "Device: " + deviceName + " Service: " + serviceName + " Characteristic: " + charName;
// we got response regarding our request to write new value to the characteristic
// let see if it failed or not
if(status == BluetoothGatt.GATT_SUCCESS) {
mUiCallback.uiSuccessfulWrite(mBluetoothGatt, mBluetoothDevice, mBluetoothSelectedService, characteristic, description);
}
else {
mUiCallback.uiFailedWrite(mBluetoothGatt, mBluetoothDevice, mBluetoothSelectedService, characteristic, description + " STATUS = " + status);
}
};
@Override
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
if(status == BluetoothGatt.GATT_SUCCESS) {
// we got new value of RSSI of the connection, pass it to the UI
mUiCallback.uiNewRssiAvailable(mBluetoothGatt, mBluetoothDevice, rssi);
}
};
};
private Activity mParent = null;
private boolean mConnected = false;
private String mDeviceAddress = "";
private BluetoothManager mBluetoothManager = null;
private BluetoothAdapter mBluetoothAdapter = null;
private BluetoothDevice mBluetoothDevice = null;
private BluetoothGatt mBluetoothGatt = null;
private BluetoothGattService mBluetoothSelectedService = null;
private List<BluetoothGattService> mBluetoothGattServices = null;
private Handler mTimerHandler = new Handler();
private boolean mTimerEnabled = false;
}
mBluetoothAdapter
是 null
,因此 startLeScan
方法调用失败
希望对您有所帮助:)
将 BleWrapper
class 的 startScanning
方法替换为以下代码,它应该可以工作。
public void startScanning() {
if(mBluetoothAdapter == null)
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
mBluetoothAdapter.startLeScan(mDeviceFoundCallback);
}
我对本书 "Getting Started with Bluetooth Low Energy" 中的教程有疑问。
我只想扫描 BLE 设备并通过 logcat post 结果。但只要我开始扫描,应用程序就会关闭并给我一个 NullPointerException(参见下面的 Logcat)。对于这个例子,我使用的是 BleWrapper Class,但我也尝试在另一个应用程序中设置没有它的 BLE 扫描。但它总是同样的错误...... NullPointerException ...... 我还尝试了 2 个不同的演示应用程序,将它们导入我的 Android 工作室(v1.1)并且它们表现良好。但是,如果我尝试在其自己的 activity 中编写相同的代码,它将因此错误而崩溃。
我猜是回调或扫描方法有问题,也许我忘记了一些参考?
这是我的主要内容 activity:
package com.example.oliver.blebuch;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Intent;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
public class MainActivity extends ActionBarActivity {
private BleWrapper mBleWrapper = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d("tag", "Test");
mBleWrapper = new BleWrapper(this, new BleWrapperUiCallbacks.Null()
{
@Override
public void uiDeviceFound(final BluetoothDevice device,
final int rssi,
final byte[] scanRecord)
{
Log.d("tag", "uiDeviceFound: "+device.getName()+", "+rssi+", "+scanRecord.toString());
}
});
if(mBleWrapper.checkBleHardwareAvailable() == false)
{
Toast.makeText(this,"No BLE-compatible hardware detected",
Toast.LENGTH_SHORT).show();
finish();
}
}
protected void onResume(){
super.onResume();
//check for Bluetooth enabled on each resume
if (mBleWrapper.isBtEnabled() == false)
{
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivity(enableBtIntent);
finish();
}
}
@Override
protected void onPause(){
super.onPause();
mBleWrapper.disconnect();
mBleWrapper.close();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId())
{
case R.id.action_scan:
mBleWrapper.startScanning();
break;
case R.id.action_stop:
mBleWrapper.stopScanning();
break;
default:
break;
}
return super.onOptionsItemSelected(item);
}
}
Logcat:
03-26 10:28:49.986 26288-26288/com.example.oliver.blebuch D/OpenGLRenderer﹕ Enabling debug mode 0
03-26 10:28:53.236 26288-26288/com.example.oliver.blebuch W/dalvikvm﹕ method Landroid/support/v7/internal/widget/ListViewCompat;.lookForSelectablePosition incorrectly overrides package-private method with same name in Landroid/widget/ListView;
03-26 10:28:53.266 26288-26288/com.example.oliver.blebuch D/AbsListView﹕ Get MotionRecognitionManager
03-26 10:28:54.366 26288-26288/com.example.oliver.blebuch D/AndroidRuntime﹕ Shutting down VM
03-26 10:28:54.366 26288-26288/com.example.oliver.blebuch W/dalvikvm﹕ threadid=1: thread exiting with uncaught exception (group=0x41783da0)
03-26 10:28:54.376 26288-26288/com.example.oliver.blebuch E/AndroidRuntime﹕ FATAL EXCEPTION: main
Process: com.example.oliver.blebuch, PID: 26288
java.lang.NullPointerException
at com.example.oliver.blebuch.BleWrapper.startScanning(BleWrapper.java:79)
at com.example.oliver.blebuch.MainActivity.onOptionsItemSelected(MainActivity.java:78)
at android.app.Activity.onMenuItemSelected(Activity.java:2708)
at android.support.v4.app.FragmentActivity.onMenuItemSelected(FragmentActivity.java:350)
at android.support.v7.app.ActionBarActivity.onMenuItemSelected(ActionBarActivity.java:155)
at android.support.v7.app.ActionBarActivityDelegate.onMenuItemSelected(ActionBarActivityDelegate.java:74)
at android.support.v7.app.ActionBarActivityDelegateBase.onMenuItemSelected(ActionBarActivityDelegateBase.java:556)
at android.support.v7.internal.view.menu.MenuBuilder.dispatchMenuItemSelected(MenuBuilder.java:802)
at android.support.v7.internal.view.menu.MenuItemImpl.invoke(MenuItemImpl.java:153)
at android.support.v7.internal.view.menu.MenuBuilder.performItemAction(MenuBuilder.java:949)
at android.support.v7.internal.view.menu.MenuBuilder.performItemAction(MenuBuilder.java:939)
at android.support.v7.internal.view.menu.MenuPopupHelper.onItemClick(MenuPopupHelper.java:187)
at android.widget.AdapterView.performItemClick(AdapterView.java:308)
at android.widget.AbsListView.performItemClick(AbsListView.java:1495)
at android.widget.AbsListView$PerformClick.run(AbsListView.java:3453)
at android.widget.AbsListView.run(AbsListView.java:4816)
at android.os.Handler.handleCallback(Handler.java:733)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:136)
at android.app.ActivityThread.main(ActivityThread.java:5479)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1283)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1099)
at dalvik.system.NativeStart.main(Native Method)
BLE 包装器 Class:
package com.example.oliver.blebuch;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.util.Log;
public class BleWrapper {
/* defines (in milliseconds) how often RSSI should be updated */
private static final int RSSI_UPDATE_TIME_INTERVAL = 1500; // 1.5 seconds
/* callback object through which we are returning results to the caller */
private BleWrapperUiCallbacks mUiCallback = null;
/* define NULL object for UI callbacks */
private static final BleWrapperUiCallbacks NULL_CALLBACK = new BleWrapperUiCallbacks.Null();
/* creates BleWrapper object, set its parent activity and callback object */
public BleWrapper(Activity parent, BleWrapperUiCallbacks callback) {
this.mParent = parent;
mUiCallback = callback;
if(mUiCallback == null) mUiCallback = NULL_CALLBACK;
}
public BluetoothManager getManager() { return mBluetoothManager; }
public BluetoothAdapter getAdapter() { return mBluetoothAdapter; }
public BluetoothDevice getDevice() { return mBluetoothDevice; }
public BluetoothGatt getGatt() { return mBluetoothGatt; }
public BluetoothGattService getCachedService() { return mBluetoothSelectedService; }
public List<BluetoothGattService> getCachedServices() { return mBluetoothGattServices; }
public boolean isConnected() { return mConnected; }
/* run test and check if this device has BT and BLE hardware available */
public boolean checkBleHardwareAvailable() {
// First check general Bluetooth Hardware:
// get BluetoothManager...
final BluetoothManager manager = (BluetoothManager) mParent.getSystemService(Context.BLUETOOTH_SERVICE);
if(manager == null) return false;
// .. and then get adapter from manager
final BluetoothAdapter adapter = manager.getAdapter();
if(adapter == null) return false;
// and then check if BT LE is also available
boolean hasBle = mParent.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);
return hasBle;
}
/* before any action check if BT is turned ON and enabled for us
* call this in onResume to be always sure that BT is ON when Your
* application is put into the foreground */
public boolean isBtEnabled() {
final BluetoothManager manager = (BluetoothManager) mParent.getSystemService(Context.BLUETOOTH_SERVICE);
if(manager == null) return false;
final BluetoothAdapter adapter = manager.getAdapter();
if(adapter == null) return false;
return adapter.isEnabled();
}
/* start scanning for BT LE devices around */
public void startScanning() {
mBluetoothAdapter.startLeScan(mDeviceFoundCallback);
}
/* stops current scanning */
public void stopScanning() {
mBluetoothAdapter.stopLeScan(mDeviceFoundCallback);
}
/* initialize BLE and get BT Manager & Adapter */
public boolean initialize() {
if (mBluetoothManager == null) {
mBluetoothManager = (BluetoothManager) mParent.getSystemService(Context.BLUETOOTH_SERVICE);
if (mBluetoothManager == null) {
return false;
}
}
if(mBluetoothAdapter == null) mBluetoothAdapter = mBluetoothManager.getAdapter();
if (mBluetoothAdapter == null) {
return false;
}
return true;
}
/* connect to the device with specified address */
public boolean connect(final String deviceAddress) {
if (mBluetoothAdapter == null || deviceAddress == null) return false;
mDeviceAddress = deviceAddress;
// check if we need to connect from scratch or just reconnect to previous device
if(mBluetoothGatt != null && mBluetoothGatt.getDevice().getAddress().equals(deviceAddress)) {
// just reconnect
return mBluetoothGatt.connect();
}
else {
// connect from scratch
// get BluetoothDevice object for specified address
mBluetoothDevice = mBluetoothAdapter.getRemoteDevice(mDeviceAddress);
if (mBluetoothDevice == null) {
// we got wrong address - that device is not available!
return false;
}
// connect with remote device
mBluetoothGatt = mBluetoothDevice.connectGatt(mParent, false, mBleCallback);
}
return true;
}
/* disconnect the device. It is still possible to reconnect to it later with this Gatt client */
public void disconnect() {
if(mBluetoothGatt != null) mBluetoothGatt.disconnect();
mUiCallback.uiDeviceDisconnected(mBluetoothGatt, mBluetoothDevice);
}
/* close GATT client completely */
public void close() {
if(mBluetoothGatt != null) mBluetoothGatt.close();
mBluetoothGatt = null;
}
/* request new RSSi value for the connection*/
public void readPeriodicalyRssiValue(final boolean repeat) {
mTimerEnabled = repeat;
// check if we should stop checking RSSI value
if(mConnected == false || mBluetoothGatt == null || mTimerEnabled == false) {
mTimerEnabled = false;
return;
}
mTimerHandler.postDelayed(new Runnable() {
@Override
public void run() {
if(mBluetoothGatt == null ||
mBluetoothAdapter == null ||
mConnected == false)
{
mTimerEnabled = false;
return;
}
// request RSSI value
mBluetoothGatt.readRemoteRssi();
// add call it once more in the future
readPeriodicalyRssiValue(mTimerEnabled);
}
}, RSSI_UPDATE_TIME_INTERVAL);
}
/* starts monitoring RSSI value */
public void startMonitoringRssiValue() {
readPeriodicalyRssiValue(true);
}
/* stops monitoring of RSSI value */
public void stopMonitoringRssiValue() {
readPeriodicalyRssiValue(false);
}
/* request to discover all services available on the remote devices
* results are delivered through callback object */
public void startServicesDiscovery() {
if(mBluetoothGatt != null) mBluetoothGatt.discoverServices();
}
/* gets services and calls UI callback to handle them
* before calling getServices() make sure service discovery is finished! */
public void getSupportedServices() {
if(mBluetoothGattServices != null && mBluetoothGattServices.size() > 0) mBluetoothGattServices.clear();
// keep reference to all services in local array:
if(mBluetoothGatt != null) mBluetoothGattServices = mBluetoothGatt.getServices();
mUiCallback.uiAvailableServices(mBluetoothGatt, mBluetoothDevice, mBluetoothGattServices);
}
/* get all characteristic for particular service and pass them to the UI callback */
public void getCharacteristicsForService(final BluetoothGattService service) {
if(service == null) return;
List<BluetoothGattCharacteristic> chars = null;
chars = service.getCharacteristics();
mUiCallback.uiCharacteristicForService(mBluetoothGatt, mBluetoothDevice, service, chars);
// keep reference to the last selected service
mBluetoothSelectedService = service;
}
/* request to fetch newest value stored on the remote device for particular characteristic */
public void requestCharacteristicValue(BluetoothGattCharacteristic ch) {
if (mBluetoothAdapter == null || mBluetoothGatt == null) return;
mBluetoothGatt.readCharacteristic(ch);
// new value available will be notified in Callback Object
}
/* get characteristic's value (and parse it for some types of characteristics)
* before calling this You should always update the value by calling requestCharacteristicValue() */
public void getCharacteristicValue(BluetoothGattCharacteristic ch) {
if (mBluetoothAdapter == null || mBluetoothGatt == null || ch == null) return;
byte[] rawValue = ch.getValue();
String strValue = null;
int intValue = 0;
// lets read and do real parsing of some characteristic to get meaningful value from it
UUID uuid = ch.getUuid();
if(uuid.equals(BleDefinedUUIDs.Characteristic.HEART_RATE_MEASUREMENT)) { // heart rate
// follow https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.heart_rate_measurement.xml
// first check format used by the device - it is specified in bit 0 and tells us if we should ask for index 1 (and uint8) or index 2 (and uint16)
int index = ((rawValue[0] & 0x01) == 1) ? 2 : 1;
// also we need to define format
int format = (index == 1) ? BluetoothGattCharacteristic.FORMAT_UINT8 : BluetoothGattCharacteristic.FORMAT_UINT16;
// now we have everything, get the value
intValue = ch.getIntValue(format, index);
strValue = intValue + " bpm"; // it is always in bpm units
}
else if (uuid.equals(BleDefinedUUIDs.Characteristic.HEART_RATE_MEASUREMENT) || // manufacturer name string
uuid.equals(BleDefinedUUIDs.Characteristic.MODEL_NUMBER_STRING) || // model number string)
uuid.equals(BleDefinedUUIDs.Characteristic.FIRMWARE_REVISION_STRING)) // firmware revision string
{
// follow https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.manufacturer_name_string.xml etc.
// string value are usually simple utf8s string at index 0
strValue = ch.getStringValue(0);
}
else if(uuid.equals(BleDefinedUUIDs.Characteristic.APPEARANCE)) { // appearance
// follow: https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.gap.appearance.xml
intValue = ((int)rawValue[1]) << 8;
intValue += rawValue[0];
strValue = BleNamesResolver.resolveAppearance(intValue);
}
else if(uuid.equals(BleDefinedUUIDs.Characteristic.BODY_SENSOR_LOCATION)) { // body sensor location
// follow: https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.body_sensor_location.xml
intValue = rawValue[0];
strValue = BleNamesResolver.resolveHeartRateSensorLocation(intValue);
}
else if(uuid.equals(BleDefinedUUIDs.Characteristic.BATTERY_LEVEL)) { // battery level
// follow: https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.battery_level.xml
intValue = rawValue[0];
strValue = "" + intValue + "% battery level";
}
else {
// not known type of characteristic, so we need to handle this in "general" way
// get first four bytes and transform it to integer
intValue = 0;
if(rawValue.length > 0) intValue = (int)rawValue[0];
if(rawValue.length > 1) intValue = intValue + ((int)rawValue[1] << 8);
if(rawValue.length > 2) intValue = intValue + ((int)rawValue[2] << 8);
if(rawValue.length > 3) intValue = intValue + ((int)rawValue[3] << 8);
if (rawValue.length > 0) {
final StringBuilder stringBuilder = new StringBuilder(rawValue.length);
for(byte byteChar : rawValue) {
stringBuilder.append(String.format("%c", byteChar));
}
strValue = stringBuilder.toString();
}
}
String timestamp = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss.SSS").format(new Date());
mUiCallback.uiNewValueForCharacteristic(mBluetoothGatt,
mBluetoothDevice,
mBluetoothSelectedService,
ch,
strValue,
intValue,
rawValue,
timestamp);
}
/* reads and return what what FORMAT is indicated by characteristic's properties
* seems that value makes no sense in most cases */
public int getValueFormat(BluetoothGattCharacteristic ch) {
int properties = ch.getProperties();
if((BluetoothGattCharacteristic.FORMAT_FLOAT & properties) != 0) return BluetoothGattCharacteristic.FORMAT_FLOAT;
if((BluetoothGattCharacteristic.FORMAT_SFLOAT & properties) != 0) return BluetoothGattCharacteristic.FORMAT_SFLOAT;
if((BluetoothGattCharacteristic.FORMAT_SINT16 & properties) != 0) return BluetoothGattCharacteristic.FORMAT_SINT16;
if((BluetoothGattCharacteristic.FORMAT_SINT32 & properties) != 0) return BluetoothGattCharacteristic.FORMAT_SINT32;
if((BluetoothGattCharacteristic.FORMAT_SINT8 & properties) != 0) return BluetoothGattCharacteristic.FORMAT_SINT8;
if((BluetoothGattCharacteristic.FORMAT_UINT16 & properties) != 0) return BluetoothGattCharacteristic.FORMAT_UINT16;
if((BluetoothGattCharacteristic.FORMAT_UINT32 & properties) != 0) return BluetoothGattCharacteristic.FORMAT_UINT32;
if((BluetoothGattCharacteristic.FORMAT_UINT8 & properties) != 0) return BluetoothGattCharacteristic.FORMAT_UINT8;
return 0;
}
/* set new value for particular characteristic */
public void writeDataToCharacteristic(final BluetoothGattCharacteristic ch, final byte[] dataToWrite) {
if (mBluetoothAdapter == null || mBluetoothGatt == null || ch == null) return;
// first set it locally....
ch.setValue(dataToWrite);
// ... and then "commit" changes to the peripheral
mBluetoothGatt.writeCharacteristic(ch);
}
/* enables/disables notification for characteristic */
public void setNotificationForCharacteristic(BluetoothGattCharacteristic ch, boolean enabled) {
if (mBluetoothAdapter == null || mBluetoothGatt == null) return;
boolean success = mBluetoothGatt.setCharacteristicNotification(ch, enabled);
if(!success) {
Log.e("------", "Seting proper notification status for characteristic failed!");
}
// This is also sometimes required (e.g. for heart rate monitors) to enable notifications/indications
// see: https://developer.bluetooth.org/gatt/descriptors/Pages/DescriptorViewer.aspx?u=org.bluetooth.descriptor.gatt.client_characteristic_configuration.xml
BluetoothGattDescriptor descriptor = ch.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
if(descriptor != null) {
byte[] val = enabled ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE;
descriptor.setValue(val);
mBluetoothGatt.writeDescriptor(descriptor);
}
}
/* defines callback for scanning results */
private BluetoothAdapter.LeScanCallback mDeviceFoundCallback = new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(final BluetoothDevice device, final int rssi, final byte[] scanRecord) {
mUiCallback.uiDeviceFound(device, rssi, scanRecord);
}
};
/* callbacks called for any action on particular Ble Device */
private final BluetoothGattCallback mBleCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
mConnected = true;
mUiCallback.uiDeviceConnected(mBluetoothGatt, mBluetoothDevice);
// now we can start talking with the device, e.g.
mBluetoothGatt.readRemoteRssi();
// response will be delivered to callback object!
// in our case we would also like automatically to call for services discovery
startServicesDiscovery();
// and we also want to get RSSI value to be updated periodically
startMonitoringRssiValue();
}
else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
mConnected = false;
mUiCallback.uiDeviceDisconnected(mBluetoothGatt, mBluetoothDevice);
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
// now, when services discovery is finished, we can call getServices() for Gatt
getSupportedServices();
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status)
{
// we got response regarding our request to fetch characteristic value
if (status == BluetoothGatt.GATT_SUCCESS) {
// and it success, so we can get the value
getCharacteristicValue(characteristic);
}
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic)
{
// characteristic's value was updated due to enabled notification, lets get this value
// the value itself will be reported to the UI inside getCharacteristicValue
getCharacteristicValue(characteristic);
// also, notify UI that notification are enabled for particular characteristic
mUiCallback.uiGotNotification(mBluetoothGatt, mBluetoothDevice, mBluetoothSelectedService, characteristic);
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
String deviceName = gatt.getDevice().getName();
String serviceName = BleNamesResolver.resolveServiceName(characteristic.getService().getUuid().toString().toLowerCase(Locale.getDefault()));
String charName = BleNamesResolver.resolveCharacteristicName(characteristic.getUuid().toString().toLowerCase(Locale.getDefault()));
String description = "Device: " + deviceName + " Service: " + serviceName + " Characteristic: " + charName;
// we got response regarding our request to write new value to the characteristic
// let see if it failed or not
if(status == BluetoothGatt.GATT_SUCCESS) {
mUiCallback.uiSuccessfulWrite(mBluetoothGatt, mBluetoothDevice, mBluetoothSelectedService, characteristic, description);
}
else {
mUiCallback.uiFailedWrite(mBluetoothGatt, mBluetoothDevice, mBluetoothSelectedService, characteristic, description + " STATUS = " + status);
}
};
@Override
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
if(status == BluetoothGatt.GATT_SUCCESS) {
// we got new value of RSSI of the connection, pass it to the UI
mUiCallback.uiNewRssiAvailable(mBluetoothGatt, mBluetoothDevice, rssi);
}
};
};
private Activity mParent = null;
private boolean mConnected = false;
private String mDeviceAddress = "";
private BluetoothManager mBluetoothManager = null;
private BluetoothAdapter mBluetoothAdapter = null;
private BluetoothDevice mBluetoothDevice = null;
private BluetoothGatt mBluetoothGatt = null;
private BluetoothGattService mBluetoothSelectedService = null;
private List<BluetoothGattService> mBluetoothGattServices = null;
private Handler mTimerHandler = new Handler();
private boolean mTimerEnabled = false;
}
mBluetoothAdapter
是 null
,因此 startLeScan
方法调用失败
希望对您有所帮助:)
将 BleWrapper
class 的 startScanning
方法替换为以下代码,它应该可以工作。
public void startScanning() {
if(mBluetoothAdapter == null)
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
mBluetoothAdapter.startLeScan(mDeviceFoundCallback);
}