Simon Simon - 6 months ago 30
Android Question

Android sending messages between fragment and service

I have a fragment with a button. When clicked it tells a service to start polling sensors and then insert the sensor data into a database on a background thread. When the button is pushed again, the service will stop. When the Stop button is pushed, there may still be tasks in the executor queue that is inserting into the DB, so during this time I want to display a progress dialog, and dismiss it once the entire queue is clear. The fragment with the button looks like this:

public class StartFragment extends Fragment implements View.OnClickListener {

Button startButton;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {

View view = inflater.inflate(R.layout.fragment_start, container, false);

startButton = (Button) view.findViewById(R.id.startButton);
startButton.setOnClickListener(this);

return view;
}

@Override
public void onClick(View v) {
if (recording has not yet started){
mainActivity.startService(new Intent(mainActivity, SensorService.class));
} else {
//I want to display a progress dialog here when the service is told to stop
//Once all executor task queue is clear, I want to dismiss this dialog
mainActivity.stopService(new Intent(mainActivity, SensorService.class));
}
}
}


When the button is clicked the first time, the following service will start:

public class SensorService extends Service implements SensorEventListener {

public static final int SCREEN_OFF_RECEIVER_DELAY = 100;

private SensorManager sensorManager = null;
private WakeLock wakeLock = null;
ExecutorService executor;
Runnable insertHandler;

private void registerListener() {
//register 4 sensor listeners (acceleration, gyro, magnetic, gravity)
}

private void unregisterListener() {
sensorManager.unregisterListener(this);
}

public BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.i(TAG, "onReceive("+intent+")");

if (!intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {
return;
}

Runnable runnable = new Runnable() {
public void run() {
Log.i(TAG, "Runnable executing...");
unregisterListener();
registerListener();
}
};

new Handler().postDelayed(runnable, SCREEN_OFF_RECEIVER_DELAY);
}
};

public void onSensorChanged(SensorEvent event) {
//get sensor values and store into 4 different arrays here

//insert into database in background thread
executor.execute(insertHandler);
}

@Override
public void onCreate() {
super.onCreate();

//get sensor manager and sensors here

PowerManager manager = (PowerManager) getSystemService(Context.POWER_SERVICE);
wakeLock = manager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);

registerReceiver(receiver, new IntentFilter(Intent.ACTION_SCREEN_OFF));

//Executor service and runnable for DB inserts
executor = Executors.newSingleThreadExecutor();
insertHandler = new InsertHandler();
}

@Override
public IBinder onBind(Intent intent) {
return null;
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);

startForeground(Process.myPid(), new Notification());
registerListener();
wakeLock.acquire();

return START_STICKY;
}

@Override
public void onDestroy() {
//Prevent new tasks from being added to thread
executor.shutdown();
try {
//Wait for all tasks to finish before we proceed
while (!executor.awaitTermination(1, TimeUnit.SECONDS)) {
Log.i(TAG, "Waiting for current tasks to finish");
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}

if (executor.isTerminated()){
//Stop everything else once the task queue is clear
unregisterReceiver(receiver);
unregisterListener();
wakeLock.release();
dbHelper.close();
stopForeground(true);

//Once the queue is clear, I want to send a message back to the fragment to dismiss the progress dialog here
}
}

class InsertHandler implements Runnable {
public void run() {
//get sensor values from 4 arrays, and insert into db here
}
}


So I want to display the dialog on the 2nd button press. Then once it is pressed again, service will stop, and I want to wait until the queue is clear and then send a dismiss event back to the fragment to dismiss the progress dialog.

Showing the dialog is easy. I can just add progress dialog code in the
onClick
method of the fragment, before
stopService
is called

I'm having difficulty with figuring out how to send a message back in
onDestroy
of the
SensorService
to dismiss that dialog

Whats the best way of doing this without resorting to external libraries?

Is there some way that the
BroadcastReceiver
I'm using in
SensorService
can be used? Or maybe it's better to create a new
Handler
in the fragment and somehow pass it through to the service so it can send a message back to the fragment?

EDIT:

I have tried the following based on one of the answers below:

Added a
MessageHandler
class to my fragment class:

public static class MessageHandler extends Handler {
@Override
public void handleMessage(Message message) {
int state = message.arg1;
switch (state) {
case 0:
stopDialog.dismiss();
break;
case 1:
stopDialog = new ProgressDialog(mainActivity);
stopDialog.setMessage("Stopping...");
stopDialog.setTitle("Saving data");
stopDialog.setProgressNumberFormat(null);
stopDialog.setCancelable(false);
stopDialog.setMax(100);
stopDialog.show();
break;
}
}
}


Created a new instance of
MessageHandler
in my fragment (tried placing this in a variety of places...same results):

public static Handler messageHandler = new MessageHandler();


The service is then started from my fragment using:

Intent startService = new Intent(mainActivity, SensorService.class);
startService.putExtra("MESSENGER", new Messenger(messageHandler));
getContext().startService(startService);


In my
SensorService
BroadcastReceiver
I create the messageHandler:

Bundle extras = intent.getExtras();
messageHandler = (Messenger) extras.get("MESSENGER");


Then I show the dialog at the very beginning of
SensorService
onDestroy
:

sendMessage("SHOW");


and dismiss it at the very end of that same method:

sendMessage("HIDE");


My
sendMessage
method looks like this:

public void sendMessage(String state) {
Message message = Message.obtain();
switch (state) {
case "SHOW":
message.arg1 = 1;
break;
case "HIDE" :
message.arg1 = 0;
break;
}
try {
messageHandler.send(message);
} catch (RemoteException e) {
e.printStackTrace();
}
}


So I can start the Service OK, but when I press it again to stop, I get this:

java.lang.RuntimeException: Unable to stop service com.example.app.SensorService@21124f0: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.os.Messenger.send(android.os.Message)' on a null object reference

and its referring to Line 105 of
SensorService
where I have
messageHandler.send(message)


Thoughts on what might be wrong?

Answer

It turns out that the code in the Edit of my original question works, but I have to shuffle around some of my code:

Bundle extras = intent.getExtras();
messageHandler = (Messenger) extras.get("MESSENGER");

The above needs to be moved to onStartCommand of SensorService instead of being in the BroadcastReceiver