Android sending messages between fragment and service
Asked Answered
R

3

3

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?

Rumen answered 20/4, 2016 at 5:7 Comment(3)
Why are you trying to send a message back to the fragment? Can't you just display the dialog at the beginning of onDestroy and then dismiss it at the end of onDestroy?User
Just seen this question/answers take a look at it.User
I cant display dialogs in onDestroy because thats in the service and doesn't operate in the UI threadRumen
R
0

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

Rumen answered 21/4, 2016 at 22:17 Comment(0)
P
3

In activity:

protected BroadcastReceiver mMessageReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, final Intent intent) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if(intent.hasExtra("someExtraMessage")){
                    doSomething(intent.getStringExtra("someExtraMessage"));
                }
            }
        });

    }
};

@Override
public void onCreate(Bundle savedInstanceState, PersistableBundle persistentState) {
    super.onCreate(savedInstanceState, persistentState);
    LocalBroadcastManager.getInstance(this).registerReceiver(mMessageReceiver,
            new IntentFilter("message-id"));
}

protected void onDestroy() {
    super.onDestroy();
    LocalBroadcastManager.getInstance(this).unregisterReceiver(mMessageReceiver);
}

public void doSomething(){
    //...
}

Then somewhere from service:

Context context = BamBamApplication.getApplicationContext(); // Can be application or activity context.
// BamBamApplicaiton extends Application ;)

Intent intent = new Intent("message-id");
intent.putExtra("someExtraMessage", "Some Message :)");
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);

Actually you are doing wrong from the very beginning :) all the services are running on main thread, so here you must better start all hard processing to async task to move this in background otherwise you will stuck your app, or you will get sudden unexpected crashes.

Here are you sample of async task that parses json api response in background with Typed result by parameter.

class ParseJsonInBackground<T> extends AsyncTask<String, Void, ApiResponseModel<T>> {
    private ProcessResponse<T> func;
    private Type inClass;

    public ParseJsonInBackground(ProcessResponse<T> f, Type inClass){
        this.func = f;
        this.inClass = inClass;
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
    }


    @Override
    protected ApiResponseModel<T> doInBackground(String... json) {
        Gson gson = new Gson();
        try {

            ApiResponseModel<T> result = (ApiResponseModel<T>) gson.fromJson(json[0], inClass);
            return result;
        }catch(Exception e){
            ApiResponseModel<T> result = new ApiResponseModel<T>();
            result.data = null;
            result.success = false;
            result.error = new ArrayList<>();
            result.error.add(new ErrorModel(0, "Parsing error", "Parsing error"));
            return result;
        }
    }


    @Override
    protected void onPostExecute(ApiResponseModel<T> result) {
        Utils.hideLoadingProgress(mContext);
        if(result != null && func != null){
            if(result.success){
                func.onSuccess(result);
            }else{
                func.onError(result);
            }
        }

    }
}

and sample how to call:

new ParseJsonInBackground<T>(responseFunc, inClass).execute(json.toString());

make attention! - don't use any views in processing coz this will stuck main thread, make database processing in similar async task, don't write to often to database make recording with transactions.

Peritoneum answered 24/5, 2016 at 23:26 Comment(0)
C
0

I would propose doing this via Handler messages: you send a message from the Service to your Activity which has to register as a callback handler (implement http://developer.android.com/reference/android/os/Handler.Callback.html). Use a custom message code (message.what) and listen for it. Keep in mind to send this to the main looper of your application (from the service).

You may also check this comment which illustrates this kind of interaction with some more code: https://mcmap.net/q/299969/-communication-between-activity-and-service

Candide answered 20/4, 2016 at 6:3 Comment(4)
I just tried the answer in the link you posted, but I get a NPE. See my question edit aboveRumen
messageHandler is null? You have to setup a new Handler: messageHandler = new Handler(getMainLooper());Candide
Arent I already doing that in the fragment with public static Handler messageHandler = new MessageHandler()? It wont let me add getMainLooper() to that. And in SensorService messageHandler is of type Messenger so I cant spawn a new Handler from it, but in my code above I'm already assigning a Messenger to it: messageHandler = (Messenger) extras.get("MESSENGER")Rumen
I dont think that you can put a MessageHandler instance into a Bundle ;) You have to define two Handlers: one is sending the message and the second one is handling it. Both have to be connected via the MainLooper, thats why you have to call getMainLooper()Candide
R
0

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

Rumen answered 21/4, 2016 at 22:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.