1

I am writing an Android App that checks for new messages in the background using AlarmManager and WakefulBroadcastReceiver. The Receiver starts an IntentService and that uses an AsyncTask for the network request (HTTPClient).

After a reboot, the alarm is re-enabled using a BroadcastReceiver listening for android.intent.action.BOOT_COMPLETED.

The problem occures when the onCreate method of my main Activity loads the full message contents using an AsyncTask, but only under certain circumstances:

java.lang.RuntimeException: Handler (android.os.AsyncTask$InternalHandler) {4139f618} sending message to a Handler on a dead thread

The doInBackground is executed and returns a String with valid content, but instead of executing onPostExecute, the excaption rises.

There are several scenarios:

  1. I start the app from the launcher -> WORKS.
  2. The App has been started like (1), sent to background and then a message arrives. The Notification is tapped, the Activity starts -> WORKS.
  3. The App has been started like (1), send to background and the device was rebooted. A message arrives, the notification is tapped, the Activity starts -> EXCEPTION.

Question: How can I fix that, without burning in the developer hell for doing network requests on the UI thread?

Searching for a solution, I found onPostExecute not being called in AsyncTask (Handler runtime exception), but neither the solution from "sdw" nor from "Jonathan Perlow" changes a thing.

ServerQuery:

public abstract class ServerQuery extends AsyncTask<Void, Void, String> {
    protected Context context;

    String user = "ERR";
    String pin = "ERR";
    String cmd = "ERR";

    ServerQuery(Context context, String _user, String _pin, String _cmd) {
        this.context = context;
        user = _user;
        pin = _pin;
        cmd = _cmd;
    }

    private ServerQuery() {
    }

    @Override
    protected String doInBackground(Void... params) {
        ArrayList<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>();
        nameValuePairs.add(new BasicNameValuePair(context.getString(R.string.post_client), context.getString(R.string.post_client_app)));
        nameValuePairs.add(new BasicNameValuePair(context.getString(R.string.post_user), user));
        nameValuePairs.add(new BasicNameValuePair(context.getString(R.string.post_pin), pin));
        nameValuePairs.add(new BasicNameValuePair(context.getString(R.string.post_cmd), cmd));
        HttpClient httpClient = new DefaultHttpClient();
        HttpPost httpPost = new HttpPost("http://[my server hostname]/index.php");
        try {
            httpPost.setEntity(new UrlEncodedFormEntity(nameValuePairs));
        } catch (UnsupportedEncodingException e) {
            return "ERR";
        }
        HttpResponse httpResponse = null;
        try{
            httpResponse = httpClient.execute(httpPost);
        }catch (Exception e) {
            return "ERR";
        }
        HttpEntity httpEntity = httpResponse.getEntity();
        InputStream inputStream = null;
        try {
            inputStream = httpEntity.getContent();
        } catch (IOException e) {
            return "ERR";
        }
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        StringBuilder stringBuilder = new StringBuilder();
        String s = null;
        try {
            while ((s = bufferedReader.readLine()) != null) {
                stringBuilder.append(s);
            }
        } catch (IOException e) {
            return "ERR";
        }
        try {
            inputStream.close();
        } catch (IOException e) {
            return "ERR";
        }
        return stringBuilder.toString();
    }
}

lastID:

public class lastID extends ServerQuery {

    Integer lastid, savedid;

    lastID(Context context, String _user, String _pin, String _cmd) {
        super(context, _user, _pin, _cmd);
        lastid = -1;
    }

    @Override
    protected void onPostExecute(final String success) {
        if(success.equals("ERR") || success.length() < 1) {
            Toast.makeText(context, context.getString(R.string.server_no_connection), Toast.LENGTH_LONG).show();
        } else {
            String content[] = success.split("(;)");
            if(content.length == 2) {
                lastid = Integer.parseInt(content[0]);
                SharedPreferences sharedPreferences = context.getSharedPreferences(context.getString(R.string.settings_filename), 0);
                SharedPreferences.Editor editor = sharedPreferences.edit();
                savedid = sharedPreferences.getInt(context.getString(R.string.post_lastid), -1);
                if (lastid > savedid) { // NEUES ANGEBOT!
                    editor.putInt(context.getString(R.string.post_lastid), lastid);
                    editor.commit();
                    // Benachrichtung
                    NotificationCompat.Builder notific = new NotificationCompat.Builder(context);
                    notific.setContentTitle(context.getString(R.string.notify_title));
                    notific.setContentText(content[1]);
                    notific.setSmallIcon(R.drawable.ic_notify);
                    notific.setAutoCancel(false);
                    notific.setPriority(NotificationCompat.PRIORITY_HIGH);
                    notific.setTicker(content[1]);
                    notific.setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE | Notification.DEFAULT_LIGHTS);

                    Intent resultIntent = new Intent(context, MainActivity.class);
                    TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
                    stackBuilder.addParentStack(MainActivity.class);
                    stackBuilder.addNextIntent(resultIntent);
                    PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_CANCEL_CURRENT);
                    notific.setContentIntent(resultPendingIntent);

                    NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
                    notificationManager.notify(lastid, notific.build());
                }
            }
        }
    }
}

MainActivity onCreate:

[ loading shared preferences, setting onclicklisteners ]
angebot = new Angebot(this, getApplicationContext(), user, pin, getString(R.string.post_cmd_viewAngebot));
angebot.execute((Void) null);

Angebot is like lastID, but it uses the String to fill the TextViews. EDIT 1: Angebot:

public class Angebot extends ServerQuery {

    protected Activity activity;

    protected TextView datumView;
    // more TextView s
    protected Button hungerButton;

    Angebot(Activity activity, Context context, String _user, String _pin, String _cmd) {
        super(context, _user, _pin, _cmd);
        this.activity = activity;

        datumView = (TextView) this.activity.findViewById(R.id.datum);
        // finding all other TextView s
    }

    @Override
    protected void onPreExecute() {
        showProgress(true);
    }

    @Override
    protected void onPostExecute(final String success) {
        Log.v(C.TAG_ALARMESSEN, "Angebot.onPostExecute");
        showProgress(false);
        if(success.equals("ERR") || success.length() < 1) {
            Toast.makeText(activity.getApplicationContext(), context.getString(R.string.server_no_connection), Toast.LENGTH_LONG).show();
        } else {
            String content[] = success.split("(;)");
                // put each content string in a TextView
            } else {
                Toast.makeText(context, context.getString(R.string.err02s), Toast.LENGTH_LONG).show(); // nicht angemeldet
            }
        }
    }

    @Override
    protected void onCancelled() {
        showProgress(false);
    }

    @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
    public void showProgress(final boolean show) {
        final View progressView = activity.findViewById(R.id.login_progress);
        progressView.setVisibility(show ? View.VISIBLE : View.GONE);
    }
}

EDIT 2: stack trace:

java.lang.RuntimeException: Handler (android.os.AsyncTask$InternalHandler) {4139f618} sending message to a Handler on a dead thread
            at android.os.MessageQueue.enqueueMessage(MessageQueue.java:196)
            at android.os.Handler.sendMessageAtTime(Handler.java:473)
            at android.os.Handler.sendMessageDelayed(Handler.java:446)
            at android.os.Handler.sendMessage(Handler.java:383)
            at android.os.Message.sendToTarget(Message.java:363)
            at android.os.AsyncTask.postResult(AsyncTask.java:300)
            at android.os.AsyncTask.access$400(AsyncTask.java:156)
            at android.os.AsyncTask$2.call(AsyncTask.java:264)
            at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:305)
            at java.util.concurrent.FutureTask.run(FutureTask.java:137)
            at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:208)
            at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1076)
            at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:569)
            at java.lang.Thread.run(Thread.java:856)
Community
  • 1
  • 1
  • I doubt you'll get an answer to your question since it's very unclear what you're trying to achieve. What does the IntentService have to do with the Activity? What component is starting which AsyncTask? What's Angebot and where is it defined? Why would you start an AsyncTask in an IntentService (already runs in its own thread)? What exception occurs (stack trace)? Without clearing things up no one will spend time to figure out what your problem is and how to solve it. – Emanuel Moecklin Mar 26 '15 at 21:25
  • I have added the class "Angebot". The AsyncTask (ServerQuery) is also used in the IntentService, because I did not want to duplicate the doInBackground code. For the special use case in the IntentService, i am calling doInBackground and onPostExecute myself, because, as you already stated, it runs in its own thread. Stack trace fowllows in a moment. – wurzelgemuese Mar 26 '15 at 21:41
  • How do you start the Activity from the notification and how does the manifest look like (the Activity part)? – Emanuel Moecklin Mar 27 '15 at 01:39
  • I think you could try to fix this or use AsyncTaskLoader instead. My guess is that the AsyncTask is started from the notification thread which dies when the notification is gone. The fix in the mentioned post should work here. Anyway maybe using a loader does some good. – Emanuel Moecklin Mar 27 '15 at 01:44
  • The Activity is started from the class "lastID" where it says " // Benachrichtigung". I will try the AsyncTaskLoader tomorrow... – wurzelgemuese Mar 27 '15 at 02:15
  • @EmanuelMoecklin Loader did it, thank you! I will post the changes to my code that did the magic, but I would accept your answer if you post your comment as an answer ;-) – wurzelgemuese Mar 27 '15 at 13:49
  • Done (and some more characters to be able to add the comment ;-). – Emanuel Moecklin Mar 27 '15 at 18:34

1 Answers1

1

I would assume that the workarounds for the AsyncTask bug mentioned here: onPostExecute not being called in AsyncTask (Handler runtime exception) would work in this case too but according to the OP they don't.

Instead of fixing the issue I would suggest to use an AsyncTaskLoader instead. There's plenty of articles about the difference (mainly the advantages) of AsyncTaskLoader compared to AsyncTask. Here are just two (good) examples): http://www.javacodegeeks.com/2013/01/android-loaders-versus-asynctask.html and http://www.androiddesignpatterns.com/2012/07/loaders-and-loadermanager-background.html

The main advantages of an AsyncTaskLoader is that they are synced with the life cycle of the Activity or Fragment that started it and that they can deal with configuration changes gracefully (unlike AsyncTasks).

Here's how you'd implement it. First you need the AsyncLoaderClass:

class ServerQuery extends AsyncTaskLoader<String> {
    String mResult;

    public ServerQuery(Context context) {
        super(context);
    }

    @Override
    protected void onStartLoading() {
        if (mResult == null) {
            forceLoad();    // no data yet -> force load it
        }
        else {
            deliverResult(mResult); // already got the data -> deliver it
        }
    }

    @Override
    public String loadInBackground() {
        // load your data...

        return mResult; // return the loaded data
    }

    @Override
    public void cancelLoadInBackground() {
        // do whatever it takes to stop loading
    }
}

Replace the "// load your data..." part with whatever you have in your doInBackground method. That's all there is to the actual AsyncTaskLoader. As you can see there's no equivalent to onPostExecute and that's the beauty of the AsyncTaskLoader, it's there to load data, nothing else. All the issues that occurred because AsyncTask tried to update the ui on the onPostExecute are gone.

The logic in onPostExecute now resides completely in the Activity/Fragment that starts the loader. Here's how you do it:

LoaderManager loaderMgr = getLoaderManager();
loaderMgr.initLoader(0, null, this);

this stands for LoaderManager.LoaderCallbacks meaning you have to implement that interface in your fragment/activity like so:

@Override
public Loader<String> onCreateLoader(int id, Bundle args) {
    return new ServerQuery(this);
}

@Override public void onLoaderReset(Loader<String> loader) {}

@Override
public void onLoadFinished(Loader<String> loader, String data) {
    // here goes your code to update the ui (previously onPostExecute);
}

onCreateLoader simply creates your loader (ServerQuery in this case) which will be managed by the LoaderManager henceforth. onLoadFinished will be called when the data is delivered to your fragment/activity allowing you to update the ui. You can check out one of my other answers to get some more information about Loaders that might help you understand how they work: https://stackoverflow.com/a/20916507/534471. Especially the part about configuration changes might be helpful.

Community
  • 1
  • 1
Emanuel Moecklin
  • 28,488
  • 11
  • 69
  • 85