0

Here is the source:

package ff.ff;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Canvas;
import android.os.Bundle;
import android.os.Looper;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.Surface.OutOfResourcesException;

public class Basic extends Activity {
    private Render view;

    public class Render extends SurfaceView implements Runnable {


        //TODO: Test if AlertDialog can be able to work while another
        //thread is running continuously.
        //
        // Failed miserably.

        //ERROR Received:
        /*
         * 07-08 17:34:51.035: E/AndroidRuntime(7356): FATAL EXCEPTION: Thread-12
         * 07-08 17:34:51.035: E/AndroidRuntime(7356): java.lang.RuntimeException: Main thread not allowed to quit
         * 07-08 17:34:51.035: E/AndroidRuntime(7356):  at android.os.MessageQueue.enqueueMessage(MessageQueue.java:191)
         * 07-08 17:34:51.035: E/AndroidRuntime(7356):  at android.os.Looper.quit(Looper.java:231)
         * 07-08 17:34:51.035: E/AndroidRuntime(7356):  at ff.ff.Basic$Render$1$1.run(Basic.java:45)
         * 07-08 17:34:51.035: E/AndroidRuntime(7356):  at java.lang.Thread.run(Thread.java:1027) 
         * 
         */


        private int r, g, b;

        private boolean running;
        private SurfaceHolder holder;
        private AlertDialog.Builder builder;
        private AlertDialog dialog;

        public Render(Context context) {
            super(context);
            holder = this.getHolder();
            r = g = b = 0;
            builder = new AlertDialog.Builder(context);
            builder.setTitle("Enter");
            builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    Log.d("Render Dialog", "Working...");
                    Log.d("Render Dialog", "Exiting the Looper loop...");
                    new Thread(new Runnable(){
                        public void run(){
                            Looper.getMainLooper().quit();
                        }
                    }).start();
                }
            });
            dialog = builder.create();
        }

        public void setLoopFlag(boolean value) {
            running = value;
        }

        public void run() {
            boolean flag = false;
            while(running) {
                if (holder.getSurface().isValid()) {
                    Canvas c = null;
                    try {
                        c = holder.getSurface().lockCanvas(null);
                    }
                    catch(IllegalArgumentException e) {
                        e.printStackTrace();
                    }
                    catch(OutOfResourcesException e) {
                        e.printStackTrace();
                    }
                    c.drawARGB(255, r, g, b);
                    r++;
                    g++;
                    b++;
                    if (r > 250 || g > 250 || b > 250) {
                        r = 0;
                        g = 0;
                        b = 0;
                    }
                    if (!flag){
                        flag = true;
                        Looper.prepare();
                        dialog.show();
                        Looper.loop();
                    }
                    holder.getSurface().unlockCanvasAndPost(c);
                }
            }
        }
    }



    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        view = new Render(this);
        view.setLoopFlag(true);
        setContentView(view);
        Thread thread = new Thread(view);
        thread.setName("Render Thread");
        thread.start();
    }
}

Do you know that when a game is finished, the game asks the player for a name, so that there would be a name and a score on a Scoreboard? Usually, it's like that. I have a game that renders all 3 objects onto the screen. When a certain condition was met, the game would show up a dialog, asking for a name and congratulates the player for finishing it.

It is this simple task of popping up a dialog for the player's name that's causing a lot of headache. The source code provided is given above.

When a thread is in a tight loop (such as a game loop), when a program wants to show a dialog to the user, what is usually the recommended way of doing this? And why is Looper.prepare() useful in such situation?

I couldn't get the gist of this. :(


EDIT (MORE INFO):

I tried using AsyncTask, and it really confuses me even more. Not that I don't want to use AsyncTask, but how could a simple "Show a dialog while background is changing colors" job is becoming harder and harder to fix??

Logcat:

07-08 20:20:02.445: E/AndroidRuntime(11085): FATAL EXCEPTION: AsyncTask #1
07-08 20:20:02.445: E/AndroidRuntime(11085): java.lang.RuntimeException: An error occured while executing doInBackground()
07-08 20:20:02.445: E/AndroidRuntime(11085):    at android.os.AsyncTask$3.done(AsyncTask.java:200)
07-08 20:20:02.445: E/AndroidRuntime(11085):    at java.util.concurrent.FutureTask$Sync.innerSetException(FutureTask.java:274)
07-08 20:20:02.445: E/AndroidRuntime(11085):    at java.util.concurrent.FutureTask.setException(FutureTask.java:125)
07-08 20:20:02.445: E/AndroidRuntime(11085):    at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:308)
07-08 20:20:02.445: E/AndroidRuntime(11085):    at java.util.concurrent.FutureTask.run(FutureTask.java:138)
07-08 20:20:02.445: E/AndroidRuntime(11085):    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1088)
07-08 20:20:02.445: E/AndroidRuntime(11085):    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:581)
07-08 20:20:02.445: E/AndroidRuntime(11085):    at java.lang.Thread.run(Thread.java:1027)
07-08 20:20:02.445: E/AndroidRuntime(11085): Caused by: java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
07-08 20:20:02.445: E/AndroidRuntime(11085):    at android.os.Handler.<init>(Handler.java:121)
07-08 20:20:02.445: E/AndroidRuntime(11085):    at android.app.Dialog.<init>(Dialog.java:122)
07-08 20:20:02.445: E/AndroidRuntime(11085):    at android.app.AlertDialog.<init>(AlertDialog.java:63)
07-08 20:20:02.445: E/AndroidRuntime(11085):    at android.app.AlertDialog.<init>(AlertDialog.java:59)
07-08 20:20:02.445: E/AndroidRuntime(11085):    at android.app.AlertDialog$Builder.create(AlertDialog.java:786)
07-08 20:20:02.445: E/AndroidRuntime(11085):    at ff.ff.Basic$DialogTask.doInBackground(Basic.java:112)
07-08 20:20:02.445: E/AndroidRuntime(11085):    at ff.ff.Basic$DialogTask.doInBackground(Basic.java:1)
07-08 20:20:02.445: E/AndroidRuntime(11085):    at android.os.AsyncTask$2.call(AsyncTask.java:185)
07-08 20:20:02.445: E/AndroidRuntime(11085):    at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:306)
07-08 20:20:02.445: E/AndroidRuntime(11085):    ... 4 more
07-08 20:20:03.276: E/msm8660.gralloc(11085): [unregister] handle 0x341330 still locked (state=c0000001)

Source:

package ff.ff;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Canvas;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.Surface.OutOfResourcesException;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class Basic extends Activity {
    private Render view;

    public class Render extends SurfaceView implements Runnable {


        //TODO: Test if AlertDialog can be able to work while another
        //thread is running continuously.
        //
        // Failed miserably.

        //ERROR Received:
        /*
         * 07-08 17:34:51.035: E/AndroidRuntime(7356): FATAL EXCEPTION: Thread-12
         * 07-08 17:34:51.035: E/AndroidRuntime(7356): java.lang.RuntimeException: Main thread not allowed to quit
         * 07-08 17:34:51.035: E/AndroidRuntime(7356):  at android.os.MessageQueue.enqueueMessage(MessageQueue.java:191)
         * 07-08 17:34:51.035: E/AndroidRuntime(7356):  at android.os.Looper.quit(Looper.java:231)
         * 07-08 17:34:51.035: E/AndroidRuntime(7356):  at ff.ff.Basic$Render$1$1.run(Basic.java:45)
         * 07-08 17:34:51.035: E/AndroidRuntime(7356):  at java.lang.Thread.run(Thread.java:1027) 
         * 
         */


        private int r, g, b;

        private boolean running;
        private SurfaceHolder holder;
        private DialogTask task;

        public Render(Context context) {
            super(context);
            holder = this.getHolder();
            task = new DialogTask(context);
            r = g = b = 0;
        }

        public void setLoopFlag(boolean value) {
            running = value;
        }

        public void run() {
            boolean flag = false;
            while(running) {
                if (holder.getSurface().isValid()) {
                    Canvas c = null;
                    try {
                        c = holder.getSurface().lockCanvas(null);
                    }
                    catch(IllegalArgumentException e) {
                        e.printStackTrace();
                    }
                    catch(OutOfResourcesException e) {
                        e.printStackTrace();
                    }
                    c.drawARGB(255, r, g, b);
                    r++;
                    g++;
                    b++;
                    if (r > 250 || g > 250 || b > 250) {
                        r = 0;
                        g = 0;
                        b = 0;
                    }
                    if (!flag){
                        flag = true;
                        Void[] v = new Void[1];
                        v[0] = null;
                        task.execute(v);
                    }
                    holder.getSurface().unlockCanvasAndPost(c);
                }
            }
        }
    }

    public class DialogTask extends AsyncTask<Void, Void, Void>{

        private Context context;
        private boolean exit;

        public DialogTask(Context c){
            context = c;
            exit = false;
        }

        @Override
        protected Void doInBackground(Void... params) {
            AlertDialog.Builder builder = new AlertDialog.Builder(context);
            builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    exit = true;
                }
            });
            builder.setTitle("Enter...");
            AlertDialog dialog = builder.create();
            dialog.show();
            return null;
        }

    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        view = new Render(this);
        view.setLoopFlag(true);
        setContentView(view);
        Thread thread = new Thread(view);
        thread.setName("Render Thread");
        thread.start();
    }
}

EDIT #2 (runOnUIThread() and onTouchEvent(MotionEvent e) lockups, source code given below:

public class Basic extends Activity {
    Render view;
    public class Render extends SurfaceView implements Runnable {
        private Activity activity;
        private SurfaceHolder holder;
        private boolean running;
        public Render(Activity a){
            super(a);
            activity = a;
            holder = this.getHolder();
            running = true;
        }

        public void run(){
            int r = 0;
            while (running){
                if (holder.getSurface().isValid()){
                    Canvas canvas = holder.lockCanvas();
                    canvas.drawARGB(255, r, 255, 255);
                    r++;
                    if (r > 255)
                        r = 0;
                    holder.unlockCanvasAndPost(canvas);
                }
            }   
        }

        public void start(){
            new Thread(this).start();
        }

        public boolean onTouchEvent(MotionEvent event){
            activity.runOnUiThread(new Runnable(){
                public void run(){
                    AlertDialog.Builder builder = new AlertDialog.Builder(activity);
                    builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {

                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            Log.d("Activity", "It worked also......");  
                        }
                    });
                    AlertDialog dialog = builder.create();
                    dialog.show();
                }
            });
            return false;
        }

        public void stop(){
            running = false;
            boolean r = true;
            while(r){
                try {
                    Thread.currentThread().join();
                    r = false;
                }
                catch(InterruptedException e) {
                    r = true;
                }
            }
        }
    }


    public void onCreate(Bundle b){
        super.onCreate(b);
        view = new Render(this);
        this.setContentView(view);
    }

    public void onPause(){
        super.onPause();
        view.stop();
    }

    public void onResume(){
        super.onResume();
        view.start();
    }
}

EDIT #3 (I think this is the final EDIT of the day)

Here is the "workaround" I've gotten so far. All credits goes to Nate for his help.

package ff.ff;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Canvas;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;


public class Basic extends Activity {
    private AlertDialog dialog;
    private AlertDialog.Builder builder;
    private BackgroundColors view;

    public class BackgroundColors extends SurfaceView implements Runnable {
        private Thread thread;
        private boolean running;
        private SurfaceHolder holder;

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

        public void run() {
            int r = 0;
            while (running){
                if (holder.getSurface().isValid()){
                    Canvas canvas = holder.lockCanvas();
                    if (r > 250)
                        r = 0;
                    r += 10;
                    canvas.drawARGB(255, r, 255, 255);
                    holder.unlockCanvasAndPost(canvas);
                }
            }
        }

        public void start() {
            running = true;
            thread = new Thread(this);
            holder = this.getHolder();
            thread.start();
        }

        public void stop() {
            running = false;
            boolean retry = true;
            while (retry){
                try {
                    thread.join();
                    retry = false;
                }
                catch(InterruptedException e) {
                    retry = true;
                }
            }
        }

        public boolean onTouchEvent(MotionEvent e){
            dialog.show();
            return false;
        }
    }

    public void onCreate(Bundle b) {
        super.onCreate(b);
        view = new BackgroundColors(this);
        this.setContentView(view);
        builder = new AlertDialog.Builder(this);
        builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                Log.d("Basic", "It worked");
            }
        });
        dialog = builder.create();
    }

    public void onPause(){
        super.onPause();
        view.stop();
    }

    public void onResume(){
        super.onResume();
        view.start();
    }
}
tom_mai78101
  • 2,383
  • 2
  • 32
  • 59
  • Providing logcat would be useful. In your case, I think [AsyncTask](http://developer.android.com/reference/android/os/AsyncTask.html) can be a solution. –  Jul 08 '12 at 11:00
  • @HaiBison I did provide the logcat. It's the gray comments that's the logcat. – tom_mai78101 Jul 08 '12 at 11:01
  • I got it, I'm sorry, my mistake. I think you should move the dialog builder to class `Basic`, use a [Handler](http://developer.android.com/reference/android/os/Handler.html) to communicate with it. About thread (prepare(), loop()…), I'm not sure. –  Jul 08 '12 at 11:01

2 Answers2

1

The reason this is crashing is that you are trying to shut down the main looper. There always needs to be at least the looper for the main (aka UI) thread.

So, never call

getMainLooper().quit();

Possibly, you want to call Looper.myLooper() instead of Looper.getMainLooper()? But, I'm not entirely sure what your program is trying to do.

You might want to have a read of this Android threading tutorial.

AsyncTask might also wind up being easier for you to use, although since I'm a little unclear on your app's function, maybe not.

Also, it at least looks like your boolean running flag isn't thread safe. It's being accessed from multiple threads with no protection. That's not causing the crash whose message you posted, I'm just pointing it out.

Edit: actually, now that I look at it, even though there's potential unsafety in your running variable, it only looks like you set it once, before you create your background thread. So, if that's your only usage, it's not unsafe ... but, the value is also never going to change. So, it's either useless, or you are somewhere else calling setLoopFlag(), which might be unsafe (?).

Nate
  • 31,017
  • 13
  • 83
  • 207
  • Looper.myLooper() is the same as Looper.getMainLooper(), because there's only 1 instance of the Looper. What I'm trying to do is to try and get a dialog to pop up, while the background is continuously rendering the background color. – tom_mai78101 Jul 08 '12 at 12:02
  • I specifically designed my program with an infinite loop, so I can test to see if my program can actually render the background color (from black to white) while at the same time seeing the dialog appearing in the front. – tom_mai78101 Jul 08 '12 at 12:05
  • I have added some more info. Please check. – tom_mai78101 Jul 08 '12 at 12:20
  • @tom_mai78101, if `myLooper()` returns the same as `getMainLooper()`, then you shouldn't be calling `myLooper().quit()`. The point is that you shouldn't try to call `quit()` on the main looper. What was the point in calling `quit()` in the first place? What are you trying to do with that call? (I'll check your AsyncTask code now ...) – Nate Jul 08 '12 at 12:30
  • If I don't call on Looper.quit(), then I wouldn't be able to use AlertDialog from the very beginning. According to Logcat, it stated that for me to use AlertDialog, I need to call on Looper.prepare(). What I'm trying to do with that call, is to stop getting user input once the dialog's OK button is pressed. I just wanted to show a dialog, while the background is rendering black to gray to white. And that's it. – tom_mai78101 Jul 08 '12 at 12:35
  • @tom_mai78101, I didn't say you shouldn't call `Looper.prepare()`. I said you shouldn't call `quit()`, if the Looper you're using is the main Looper . I'm going to post a second answer concerning your AsyncTask implementation, because it's really unrelated to the reason for your original crash. – Nate Jul 08 '12 at 12:38
  • @tom_mai78101, ok I can see your `onTouchEvent()` update now. – Nate Jul 08 '12 at 13:42
1

This answer pertains to the question's update, where you are trying to use AsyncTask. The code you have is actually the reverse of the way AsyncTask is intended to be used. An AsyncTask has multiple methods, that are intended to be run from different threads. The method you implemented, doInBackground() is meant to be called from the background thread. So, you should not be updating the UI (directly) in that method.

The method that runs at the end of an AsyncTask is onPostExecute(). That is run on the UI thread, and is safe to make UI calls in, such as showing a Dialog. If you need to update the UI during the running of the task, then there's a third method, onProgressUpdate(), that you can implement. It's also safe for UI operations. If your background processing (in doInBackground()), needs to communicate information to the onProgressUpdate() method, then it can do so by calling the publishProgress() method, passing in whatever data is needed in onProgressUpdate(). The parameters to those calls are generic, so you can make them almost anything. A typical implementation passes the % complete, as an integer.

See the very start of the API docs for AsyncTask for a really simple example.

But, it's sounding like something even simpler would work for you. If you change the constructor of your Render class to take an Activity, instead of a Context:

    private Activity parent;

    public Render(Activity activity) {
       super(activity);
       parent = activity;

then, you can use the super-useful runOnUiThread() method in Activity:

    parent.runOnUiThread(new Runnable() {
       public void run() {
          AlertDialog.Builder builder = new AlertDialog.Builder(parent);
          builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
             @Override
             public void onClick(DialogInterface dialog, int which) {
                 exit = true;
             }
          });
          builder.setTitle("Enter...");
          AlertDialog dialog = builder.create();
          dialog.show();
       }
    });

The above block of code is safe to put anywhere. You can put it in your doInBackground() method, or the run() method of a Runnable in a background thread. Try that.

Nate
  • 31,017
  • 13
  • 83
  • 207
  • I just read your last comment, and this answer. I felt like I'm going around and around in circles with Looper.prepare() and AsyncTask. May I ask why Logcat is telling me to use Looper.prepare(), if I'm not supposed to call on Looper.myLooper().quit()? And yes, I'm looking into your answers. – tom_mai78101 Jul 08 '12 at 13:13
  • calling `Looper.prepare()` is **one** way to solve your problem. I think using `AsyncTask` (with no `Looper` at all) is easier. And, I think using `runOnUiThread()` is easier still. In any case, even if you did insist on staying with `Looper`, you simply cannot call `quit()` on the main `Looper`. – Nate Jul 08 '12 at 13:23
  • Do you mean that I just need to call on Looper.prepare() and nothing else? HOLY MOSES... – tom_mai78101 Jul 08 '12 at 13:32
  • So, for AsyncTask, I have gotten myself confused many times. For the runOnUIThread() method, I found out it works, but it's prone to locking up my phone. I decided to call on runOnUIThread() in SurfaceView.onTouchEvent(MotionEvent), and it locks up, as expected. Right now, I'm looking into the problem. The source code for the runOnUIThread() lockup inside OnTouchEvent() is given above. – tom_mai78101 Jul 08 '12 at 13:36
  • No. I mean you shouldn't use `Looper` at all. I gave you two alternatives to using Looper above. If you look at the link I posted in the first answer, the article discusses these different approaches, and in my opinion, leaves the reader with an understanding of why `AsyncTask` is a preferable solution in most cases. But, also in that article, is does say that you would need to follow `prepare()` with `loop()`. But, not `quit()` if you're using the main Looper. – Nate Jul 08 '12 at 13:37
  • @tom_mai78101, I can't see your `onTouchEvent()` code, but if you're in code that's called on the UI thread, there's no need to call `runOnUiThread()`. You're already on the UI thread. – Nate Jul 08 '12 at 13:40
  • For the Looper, after you call on Looper.loop(), what would happen next if I didn't call on Looper.quit()? Shouldn't we call on Looper.quit() if somewhere in our code we actually called on Looper.loop()? Else, VM wouldn't be able to do some cleaning if there are objects created in the loop, and would cause memory leaks. – tom_mai78101 Jul 08 '12 at 13:41
  • the onTouchEvent() code is within the Render class. Does this mean that the onTouchEvent() for SurfaceView class is actually run on the Activity's UI Thread? If so, it might explain the lockups I'm having. :/ – tom_mai78101 Jul 08 '12 at 13:48
  • @tom_mai78101, yes, but not for the reason you mention. `onTouchEvent()` is called from the UI thread, because it's the UI thread that detects touch events, and thus invokes your touch handling callbacks (like `onTouchEvent`). As I said in my previous comment, if you're in a method that will be called on the UI thread, there's absolutely no need to call `runOnUiThread()`. Previously, you kept running that block of code in places that were running on background threads, which is why I suggested `runOnUiThread()`. – Nate Jul 08 '12 at 13:50
  • I see. I removed the runOnUIThread(), took out the codes from the inside, moved it out into the onTouchEvent(), and just tell the program to show the dialog. I think it works normally right now. – tom_mai78101 Jul 08 '12 at 13:55
  • @tom_mai78101, yep. If `onTouchEvent()` is where you want to trigger the `Dialog`, then just do it directly. No need for a Runnable there. You only need to call `runOnUiThread()` if you're not already on the UI thread. – Nate Jul 08 '12 at 13:56
  • Welp, there goes my 3 months of work... and your afternoon. Thank you Nate! – tom_mai78101 Jul 08 '12 at 13:58
  • @tom_mai78101, you're welcome. It's actually my morning, and I'm watching the Wimbledon final on my other screen :) – Nate Jul 08 '12 at 14:00