I try to run a test for my android app but I get this trace. What does it mean?
java.lang.RuntimeException: Can't toast on a thread that has not called Looper.prepare() at android.widget.Toast$TN.<init>(Toast.java:390) at android.widget.Toast.<init>(Toast.java:114) at android.widget.Toast.makeText(Toast.java:277) at android.widget.Toast.makeText(Toast.java:267) at dev.android.gamex.CatchGame.onDraw(MainActivity.java:317) at dev.android.gamex.JamieTest.useAppContext(JamieTest.java:45) at java.lang.reflect.Method.invoke(Native Method) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.mockito.internal.runners.JUnit45AndHigherRunnerImpl.run(JUnit45AndHigherRunnerImpl.java:37) at org.mockito.runners.MockitoJUnitRunner.run(MockitoJUnitRunner.java:62) at org.junit.runners.Suite.runChild(Suite.java:128) at org.junit.runners.Suite.runChild(Suite.java:27) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at org.junit.runner.JUnitCore.run(JUnitCore.java:115) at android.support.test.internal.runner.TestExecutor.execute(TestExecutor.java:58) at android.support.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:375) at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2074) Tests ran to completion.
My test class
package dev.android.gamex; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.support.test.InstrumentationRegistry; import android.widget.TextView; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class JamieTest { private static final String FAKE_STRING = "HELLO WORLD"; private OnScoreListener onScoreListener = new OnScoreListener() { @Override public void onScore(int score) { } }; @Mock Canvas can; @Test public void useAppContext() throws Exception { Context appContext = InstrumentationRegistry.getTargetContext(); assertEquals("dev.android.gamex", appContext.getPackageName()); CatchGame cg = new CatchGame(appContext, 5, "Jamie", onScoreListener); cg.initialize(); assertTrue(! cg.gameOver); cg.onDraw(new Canvas()); assertTrue(! cg.paused); } }
The code I want to test is below.
package dev.android.gamex; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; import android.support.v4.view.MotionEventCompat; import android.support.v7.app.AppCompatActivity; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.Window; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.LinearLayout; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; import java.util.Random; public class MainActivity extends AppCompatActivity { CatchGame cg; public TextView textView; public LinearLayout mainLayout; String[] spinnerValue = {"Rookie", "Advanced", "Expert", "Master"}; // start app @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mainLayout = new LinearLayout(this); mainLayout.setOrientation(LinearLayout.VERTICAL); LinearLayout menuLayout = new LinearLayout(this); menuLayout.setBackgroundColor(Color.parseColor("#FFFFFF")); textView = new TextView(this); textView.setVisibility(View.VISIBLE); String str = "Score: 0"; textView.setText(str); menuLayout.addView(textView); Button button = new Button(this); button.setText("Pause"); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { togglePausePlay(); } }); menuLayout.addView(button); Spinner spinner2 =new Spinner(this); ArrayAdapter<String> adapter = new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_list_item_1, spinnerValue); spinner2.setAdapter(adapter); menuLayout.addView(spinner2); mainLayout.addView(menuLayout); cg = new CatchGame(this, 5, "Jamie", onScoreListener); cg.setBackground(getResources().getDrawable(R.drawable.bg_land_mdpi)); mainLayout.addView(cg); getWindow().requestFeature(Window.FEATURE_ACTION_BAR); getSupportActionBar().hide(); setContentView(mainLayout); } private void togglePausePlay() { if (cg.paused) { // play // getSupportActionBar().hide(); Toast.makeText(MainActivity.this, "Play", Toast.LENGTH_SHORT).show(); } else { // pause // getSupportActionBar().show(); Toast.makeText(MainActivity.this, "Pause", Toast.LENGTH_SHORT).show(); } cg.paused = !cg.paused; } private OnScoreListener onScoreListener = new OnScoreListener() { @Override public void onScore(int score) { textView.setText("Score: " + score); } }; @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main_menu, menu); return true; } // method called when top right menu is tapped @Override public boolean onOptionsItemSelected(MenuItem item) { super.onOptionsItemSelected(item); int difficulty = cg.NBRSTEPS; String name = cg.heroName; switch (item.getItemId()) { case R.id.item11: cg = new CatchGame(this, 3, name, onScoreListener); setContentView(cg); mainLayout.addView(cg); getWindow().requestFeature(Window.FEATURE_ACTION_BAR); getSupportActionBar().hide(); setContentView(mainLayout); return true; case R.id.item12: cg = new CatchGame(this, 5, name, onScoreListener); setContentView(cg); mainLayout.addView(cg); getWindow().requestFeature(Window.FEATURE_ACTION_BAR); getSupportActionBar().hide(); setContentView(mainLayout); return true; case R.id.item13: cg = new CatchGame(this, 7, name, onScoreListener); setContentView(cg); mainLayout.addView(cg); getWindow().requestFeature(Window.FEATURE_ACTION_BAR); getSupportActionBar().hide(); setContentView(mainLayout); return true; case R.id.item14: cg = new CatchGame(this, 9, name, onScoreListener); setContentView(cg); mainLayout.addView(cg); getWindow().requestFeature(Window.FEATURE_ACTION_BAR); getSupportActionBar().hide(); setContentView(mainLayout); return true; case R.id.item15: cg = new CatchGame(this, 11, name, onScoreListener); setContentView(cg); mainLayout.addView(cg); getWindow().requestFeature(Window.FEATURE_ACTION_BAR); getSupportActionBar().hide(); setContentView(mainLayout); return true; case R.id.item21: cg = new CatchGame(this, difficulty, "Jamie", onScoreListener); setContentView(cg); mainLayout.addView(cg); getWindow().requestFeature(Window.FEATURE_ACTION_BAR); getSupportActionBar().hide(); setContentView(mainLayout); return true; case R.id.item22: cg = new CatchGame(this, difficulty, "Spaceship", onScoreListener); setContentView(cg); //mainLayout.addView(cg); //getWindow().requestFeature(Window.FEATURE_ACTION_BAR); getSupportActionBar().hide(); //setContentView(mainLayout); return true; default: cg.paused = true; return super.onOptionsItemSelected(item); } } } interface OnScoreListener { void onScore(int score); } class CatchGame extends View { int NBRSTEPS; // number of discrete positions in the x-dimension; must be uneven String heroName; int screenW; int screenH; int[] x; // x-coordinates for falling objects int[] y; // y-coordinates for falling objects int[] hero_positions; // x-coordinates for hero Random random = new Random(); int ballW; // width of each falling object int ballH; // height of ditto float dY; //vertical speed Bitmap falling, hero, jamie2, jamieleft, jamieright; int heroXCoord; int heroYCoord; int xsteps; int score; int offset; boolean gameOver; // default value is false boolean toastDisplayed; boolean paused = false; OnScoreListener onScoreListener; // constructor, load images and get sizes public CatchGame(Context context, int difficulty, String name, OnScoreListener onScoreListener) { super(context); NBRSTEPS = difficulty; heroName = name; this.onScoreListener = onScoreListener; x = new int[NBRSTEPS]; y = new int[NBRSTEPS]; hero_positions = new int[NBRSTEPS]; int resourceIdFalling = 0; int resourceIdHero = 0; if (heroName.equals("Jamie")) { resourceIdFalling = R.mipmap.falling_object2; resourceIdHero = R.drawable.left_side_hdpi; setBackground(getResources().getDrawable(R.mipmap.background)); } if (heroName.equals("Spaceship")) { resourceIdFalling = R.mipmap.falling_object; resourceIdHero = R.mipmap.ufo; setBackground(getResources().getDrawable(R.mipmap.space)); } falling = BitmapFactory.decodeResource(getResources(), resourceIdFalling); //load a falling image hero = BitmapFactory.decodeResource(getResources(), resourceIdHero); //load a hero image jamieleft = BitmapFactory.decodeResource(getResources(), R.drawable.left_side_hdpi); //load a hero image jamieright = BitmapFactory.decodeResource(getResources(), R.drawable.right_side_hdpi); //load a hero image ballW = falling.getWidth(); ballH = falling.getHeight(); } public CatchGame(Context context, int difficulty, String name, OnScoreListener onScoreListener, Drawable background) { this(context, difficulty, name, onScoreListener); this.setBackground(background); } // set coordinates, etc. void initialize() { if (!gameOver) { // run only once, when the game is first started int maxOffset = (NBRSTEPS - 1) / 2; for (int i = 0; i < x.length; i++) { int origin = (screenW / 2) + xsteps * (i - maxOffset); x[i] = origin - (ballW / 2); hero_positions[i] = origin - hero.getWidth(); } int heroWidth = hero.getWidth(); int heroHeight = hero.getHeight(); hero = Bitmap.createScaledBitmap(hero, heroWidth * 2, heroHeight * 2, true); hero = Bitmap.createScaledBitmap(hero, heroWidth * 2, heroHeight * 2, true); jamieleft = Bitmap.createScaledBitmap(jamieleft, jamieleft.getWidth()* 2, jamieright.getWidth() * 2, true); jamieright = Bitmap.createScaledBitmap(jamieright, jamieright.getWidth()* 2, jamieright.getWidth() * 2, true); heroYCoord = screenH - 2 * heroHeight; // bottom of screen } for (int i = 0; i < y.length; i++) { y[i] = -random.nextInt(1000); // place items randomly in vertical direction } offset = (NBRSTEPS - 1) / 2; // place hero at centre of the screen heroXCoord = hero_positions[offset]; // initialize or reset global attributes dY = 2.0f; score = 0; gameOver = false; toastDisplayed = false; } // method called when the screen opens @Override public void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); screenW = w; screenH = h; xsteps = w / NBRSTEPS; initialize(); } // method called when the "game over" toast has finished displaying void restart(Canvas canvas) { toastDisplayed = true; initialize(); draw(canvas); } // update the canvas in order to display the game action @Override public void onDraw(Canvas canvas) { if (toastDisplayed) { restart(canvas); return; } super.onDraw(canvas); int heroHeight = hero.getHeight(); int heroWidth = hero.getWidth(); int heroCentre = heroXCoord + heroWidth / 2; Context context = this.getContext(); // compute locations of falling objects for (int i = 0; i < y.length; i++) { if (!paused) { y[i] += (int) dY; } // if falling object hits bottom of screen if (y[i] > (screenH - ballH) && !gameOver) { dY = 0; gameOver = true; paused = true; int duration = Toast.LENGTH_SHORT; final Toast toast = Toast.makeText(context, "GAME OVER!\nScore: " + score, duration); toast.show(); Handler handler = new Handler(); handler.postDelayed(new Runnable() { @Override public void run() { toast.cancel(); toastDisplayed = true; } }, 3000); //Vibrator v = (Vibrator) context.getSystemService(context.VIBRATOR_SERVICE); // Vibrate for 3000 milliseconds //v.vibrate(3000); } // if the hero catches a falling object if (x[i] < heroCentre && x[i] + ballW > heroCentre && y[i] > screenH - ballH - heroHeight) { y[i] = -random.nextInt(1000); // reset to new vertical position score += 1; onScoreListener.onScore(score); } } canvas.save(); //Save the position of the canvas. for (int i = 0; i < y.length; i++) { canvas.drawBitmap(falling, x[i], y[i], null); //Draw the falling on the canvas. } canvas.drawBitmap(hero, heroXCoord, heroYCoord, null); //Draw the hero on the canvas. canvas.restore(); //Call the next frame. invalidate(); } // event listener for when the user touches the screen @Override public boolean onTouchEvent(MotionEvent event) { if (paused) { paused = false; } int action = MotionEventCompat.getActionMasked(event); if (action != MotionEvent.ACTION_DOWN || gameOver) { // non-touchdown event or gameover return true; // do nothing } int coordX = (int) event.getX(); int xCentre = (screenW / 2) - (hero.getWidth() / 2); int maxOffset = hero_positions.length - 1; // can't move outside right edge of screen int minOffset = 0; // ditto left edge of screen if (coordX < xCentre && offset > minOffset) { // touch event left of the centre of screen offset--; // move hero to the left if(coordX < heroXCoord)// + heroWidth / 2) hero = Bitmap.createScaledBitmap(jamieleft, jamieleft.getWidth() , jamieleft.getHeight() , true); } if (coordX > xCentre && offset < maxOffset) { // touch event right of the centre of screen offset++; // move hero to the right if(coordX > heroXCoord) hero = Bitmap.createScaledBitmap(jamieright, jamieright.getWidth() , jamieright.getHeight() , true); } heroXCoord = hero_positions[offset]; return true; } }
The repository is available online.
You CANNOT show a Toast on non-UI thread. You need to call Toast. makeText() (and most other functions dealing with the UI) from within the main thread. runOnUiThread(new Runnable() { public void run() { final Toast toast = Toast.
android.os.Looper. Class used to run a message loop for a thread. Threads by default do not have a message loop associated with them; to create one, call prepare() in the thread that is to run the loop, and then loop() to have it process messages until the loop is stopped.
User Interface Thread or UI-Thread in Android is a Thread element responsible for updating the layout elements of the application implicitly or explicitly. This means, to update an element or change its attributes in the application layout ie the front-end of the application, one can make use of the UI-Thread.
A Handler allows you to send and process Message and Runnable objects associated with a thread's MessageQueue . Each Handler instance is associated with a single thread and that thread's message queue. When you create a new Handler it is bound to a Looper .
You CANNOT show a Toast
on non-UI thread. You need to call Toast.makeText()
(and most other functions dealing with the UI) from within the main thread.
You could use Activity#runOnUiThread():
runOnUiThread(new Runnable() { public void run() { final Toast toast = Toast.makeText(context, "GAME OVER!\nScore: " + score, duration); toast.show(); } });
If you want execute a instrumentation test on main thread, add @UiThreadTest
annotation:
@Test @UiThreadTest public void useAppContext() { // ... }
P.s: There are also many other ways with explain (using Handler, Looper, Observable..) in these posts: Android: Toast in a thread and Can't create handler inside thread that has not called Looper.prepare()
One cannot show a Toast from a non UI Thread. So you can do the following from the worker thread and it doesn't require Activity or Context
JAVA
new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { Toast toast = Toast.makeText(mContext, "Toast", Toast.LENGTH_SHORT); toast.show(); } });
KOTLIN
Handler(Looper.getMainLooper()).post { Toast.makeText(mContext, "Toast", Toast.LENGTH_SHORT).show() }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With