I am fairly new to Android Studio, and App development in general. My background is Embedded firmware. To summarize, I am writing an App that will connect to a BLE enabled hardware device. The App will allow some setup on the device, and then the App will continue to run a Service in the background in order to periodically scan for the hardware device.
I have set up the service as follows (only relevant parts included in the code below):
public class MyBLEService extends Service {
public MyBLEService() {
}
/**
* Stops scanning after 8 seconds.
*/
private static final long SCAN_PERIOD = 8000;
private static boolean isRunning;
private static boolean BackgroundScan;
private static int ActivityStatus;
public static int LastCommand;
public static int NumBytesToSend;
private char CommandErrorCode;
private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
private Bundle myBundle = new Bundle();
private static BluetoothAdapter myBluetoothAdapter;
private static ScanCallback mScanCallback;
private static BluetoothGatt mBluetoothGATT;
private static BluetoothLeScanner mBluetoothLeScanner;
private static BluetoothDevice mBluetoothDevice;
private static BluetoothGattCharacteristic DE_Characteristic;
public static String ServiceID;
private static final String NOTIFICATION_CHANNEL_ID ="notification_channel_id";
private static final String NOTIFICATION_Service_CHANNEL_ID = "service_channel";
private Runnable ScanDelayRunnable;
private LooperThread myThread;
private static Handler mHandler;
private byte[] HashInput = new byte[100]; // Unencrypted data for encryption algorithm
private byte[] HashOutput = new byte[100]; // Output buffer for encrypted or decrypted data.
private byte[] PayloadData = new byte[100]; // Raw payload data (if required)
private final byte[] EmptyArray = new byte[100]; // Raw payload data (if required)
private static char temp_16;
private static char CheckSum;
private static FragmentManager fm;
private void startInForeground() {
int icon = R.mipmap.ic_launcher;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
icon = R.mipmap.ic_launcher;
}
Intent notificationIntent = new Intent(this, MyBLEService.class);
PendingIntent pendingIntent=PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(icon)
.setContentIntent(pendingIntent)
.setContentTitle("Got-U BLE Service")
.setContentText("Running");
Notification notification=builder.build();
if(Build.VERSION.SDK_INT>=26) {
NotificationChannel channel = new NotificationChannel(NOTIFICATION_Service_CHANNEL_ID, "Sync Service", NotificationManager.IMPORTANCE_HIGH);
channel.setDescription("Service Name");
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.createNotificationChannel(channel);
notification = new Notification.Builder(this,NOTIFICATION_Service_CHANNEL_ID)
.setContentTitle("Got-U BLE Service")
.setContentText("Running")
.setSmallIcon(icon)
.setContentIntent(pendingIntent)
.build();
}
startForeground(2566, notification);
}
private static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars);
}
public void BLEServiceSetBluetoothAdapter(BluetoothAdapter btAdapter) {
myBluetoothAdapter = btAdapter;
mBluetoothLeScanner = myBluetoothAdapter.getBluetoothLeScanner();
}
public void BLEServiceSetFragmentManager(FragmentManager myFM) {
fm = myFM;
}
private void sendDataActivity(String action, String name, String value)
{
Intent sendLevel = new Intent();
sendLevel.setAction(action);
sendLevel.putExtra(name, value);
sendBroadcast(sendLevel);
}
class LooperThread extends Thread {
public Handler ThreadHandler;
public void run() {
super.run();
Looper.prepare();
ThreadHandler = new Handler(Looper.myLooper()) {
@SuppressLint("MissingPermission")
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch(msg.what){
case Constants.ServiceDoNothing:
Log.d("BLE Service", "serviceHandler Value in ServiceDoNothing in Handler: " + myThread.ThreadHandler);
break;
case Constants.ServiceScanForHub:
startScanning(false);
break;
case Constants.ServiceGetCharacteristics:
mBluetoothGATT.discoverServices();
break;
case Constants.ServiceDisconnect:
mBluetoothGATT.close();
break;
case Constants.ServiceEnableNotifications:
setCharacteristicNotification(DE_Characteristic, true);
break;
case Constants.ServiceSendCommand:
// The BLE WriteCharacteristic needs a buffer with the exact
// size of bytes to send. Since HashOutput has a fixed size, we
// need to create another temporary buffer that is the size
// of NumReturnBytes, and copy the data in HashOutput to that buffer:
byte[] CommandSendBuffer = new byte[NumBytesToSend];
System.arraycopy(msg.obj,0,CommandSendBuffer,0,NumBytesToSend);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
mBluetoothGATT.writeCharacteristic(DE_Characteristic, CommandSendBuffer, BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
}
else{
DE_Characteristic.setValue(CommandSendBuffer);
boolean success = mBluetoothGATT.writeCharacteristic(DE_Characteristic);
}
break;
default: break;
}
}
};
Looper.loop();
}
}
@Override
public void onCreate() {
// Start up the thread running the service. Note that we create a
// separate thread because the service normally runs in the process's
// main thread, which we don't want to block. We also make it
// background priority so CPU-intensive work doesn't disrupt our UI.
Log.d("BLE Service", "Creating service thread");
myThread = new LooperThread();
myThread.start();
isRunning = false;
ActivityStatus = Constants.ServiceDoNothing;
MainActivity.ServiceIsActive = false;
mHandler = new Handler();
mScanCallback = null;
startInForeground();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//Toast.makeText(this, "service started", Toast.LENGTH_SHORT).show();
Log.d("BLE Service", "Starting service");
isRunning = true;
Log.d("BLE Service", "serviceHandler Value in OnStart: " + myThread.ThreadHandler);
SetActivityStatus(Constants.ServiceDoNothing);
Log.d("BLE Service", "serviceHandler Value in OnStart after sendMessage: " + myThread.ThreadHandler);
MainActivity.ServiceIsActive = true;
sendDataActivity(Constants.MessageFromBLEService, Constants.BLEServiceState, Constants.BLEServiceReady);
// If we get killed, after returning from here, restart
return START_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
// We don't provide binding, so return null
return null;
}
@Override
public void onDestroy() {
MainActivity.ServiceIsActive = false;
isRunning = false;
Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show();
}
private void DelayedMessageHandler(int what, Object object) {
new Handler().postDelayed(new Runnable() {
public void run() {
Message m = myThread.ThreadHandler.obtainMessage();
m.what = what;
if(object != null){
m.obj = object;
}
myThread.ThreadHandler.sendMessage(m);
}
}, 300);
}
public int GetActivityStatus() {
return ActivityStatus;
}
public void SubmitPassword(String password, int type) {
// Copy password characters into PayloadData:
char[] array;
// Split up password into array of char. since we cannot
// directly index the string as an array:
array = password.toCharArray();
for(int i =0;i<array.length;i++){
PayloadData[i] = (byte) array[i];
}
// Now set/submit the password:
if(type == Constants.NEW_PASSWORD)
LastCommand = Constants.DE_CMD_SET_PASSWORD;
else
LastCommand = Constants.DE_CMD_SUBMIT_PASSWORD;
NumBytesToSend = AssembleCommandResponse(Constants.DATA_PACKET_TYPE_COMMAND,
Constants.ERROR_NO_ERROR,
(byte) LastCommand,
(char) array.length,
(char) 0xFFFF,
(char) array.length,
true);
SetActivityStatus(Constants.ServiceSendCommand);
}
public void SetActivityStatus(int status) {
ActivityStatus = status;
//Log.d("BLE Service", "serviceHandler Value in SetActivityStatus: " + serviceHandler);
switch(status){
case Constants.ServiceDoNothing:
Log.d("BLE Service", "serviceHandler Value in ServiceDoNothing: " + myThread.ThreadHandler);
break;
case Constants.ServiceScanForHub:
DelayedMessageHandler(Constants.ServiceScanForHub, null);
break;
case Constants.ServiceGetCharacteristics:
DelayedMessageHandler(Constants.ServiceGetCharacteristics, null);
break;
case Constants.ServiceEnableNotifications:
DelayedMessageHandler(Constants.ServiceEnableNotifications, null);
break;
case Constants.ServiceSendCommand:
DelayedMessageHandler(Constants.ServiceSendCommand, HashOutput);
break;
default: break;
}
}
...
}
I create a class LooperThread. I then create the thread myThread in onCreate of the service. I start the service by calling
startService(new Intent(this, MyBLEService.class));
in MainActivity.
In onStartCommand of the service I initialize several parameters, and then I call the local procedure
sendDataActivity(Constants.MessageFromBLEService, Constants.BLEServiceState, Constants.BLEServiceReady);
This procedure informs MainActivity that the service is up and running. Main will now enable a fragment that shows an activity spinner, and it starts to communicate with the hardware device via BLE. The way it does this is to call this procedure in the Service with the case Constants.ServiceScanForHub:
public void SetActivityStatus(int status) {
ActivityStatus = status;
//Log.d("BLE Service", "serviceHandler Value in SetActivityStatus: " + serviceHandler);
switch(status){
case Constants.ServiceDoNothing:
Log.d("BLE Service", "serviceHandler Value in ServiceDoNothing: " + myThread.ThreadHandler);
break;
case Constants.ServiceScanForHub:
DelayedMessageHandler(Constants.ServiceScanForHub, null);
break;
case Constants.ServiceGetCharacteristics:
DelayedMessageHandler(Constants.ServiceGetCharacteristics, null);
break;
case Constants.ServiceEnableNotifications:
DelayedMessageHandler(Constants.ServiceEnableNotifications, null);
break;
case Constants.ServiceSendCommand:
DelayedMessageHandler(Constants.ServiceSendCommand, HashOutput);
break;
default: break;
}
}
This in turn calls DelayedMessageHandler(Constants.ServiceScanForHub, null);
private void DelayedMessageHandler(int what, Object object) {
new Handler().postDelayed(new Runnable() {
public void run() {
Message m = myThread.ThreadHandler.obtainMessage();
m.what = what;
if(object != null){
m.obj = object;
}
myThread.ThreadHandler.sendMessage(m);
}
}, 300);
}
And here's where I run into the problem, specifically in this line:
Message m = myThread.ThreadHandler.obtainMessage();
The value of ThreadHandler is null, even though it was not null when the thread was created. So, of course, a Null Pointer Exception is raised and the App crashes! I have done a lot of reading and debugging, but I cannot figure out what is going on. Here is the logcat info:
2023-07-23 10:40:42.243 30762-30762 AndroidRuntime com.example.got_u E FATAL EXCEPTION: main
Process: com.example.got_u, PID: 30762
java.lang.NullPointerException: Attempt to read from field 'android.os.Handler com.example.got_u.MyBLEService$LooperThread.ThreadHandler' on a null object reference
at com.example.got_u.MyBLEService$1.run(MyBLEService.java:350)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7656)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
2023-07-23 10:40:42.266 30762-30762 Process com.example.got_u I Sending signal. PID: 30762 SIG: 9
---------------------------- PROCESS ENDED (30762) for package com.example.got_u ----------------------------
And here is the Manifest info:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- Request legacy Bluetooth permissions on older devices. -->
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" /> <!-- Before Android 12 (but still needed location, even if not requested) -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <!-- From Android 12 -->
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="true" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.GotU"
tools:targetApi="31">
<service
android:name=".MyBLEService"
android:foregroundServiceType="connectedDevice"
android:process=":connectedDevice_process"
android:enabled="true"
android:exported="false">
</service>
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
I've really been struggling with this one, and I am hoping that a kind soul will be able to suggest a solution. Thanks all!
I tried to add a lot of debugging information to logcat. Also, tons of reading online (mostly in stackOverflow), unfortunately without any results so far.