I wrote an Android app that displays a custom ImageView
that rotates itself periodically, using startAnimation(Animation)
. The app works fine, but if I create a JUnit test of type ActivityInstrumentationTestCase2
and the test calls getActivity()
, that call to getActivity()
never returns until the app goes to the background (for example, the device's home button is pressed).
After much time and frustration, I found that getActivity()
returns immediately if I comment out the call to startAnimation(Animation)
in my custom ImageView
class. But that would defeat the purpose of my custom ImageView
, because I do need to animate it.
Can anyone tell me why getActivity()
blocks during my JUnit test but only when startAnimation
is used? Thanks in advance to anyone who can suggest a workaround or tell me what I'm doing wrong.
Note: the solution needs to work with Android API level 10 minimum.
Here is all the source code you need to run it (put any PNG image in res/drawable and call it the_image.png):
activity_main.xml:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity" >
<com.example.rotatingimageviewapp.RotatingImageView
android:id="@+id/rotatingImageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/the_image" />
</RelativeLayout>
MainActivity.java:
package com.example.rotatingimageviewapp;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
public class MainActivity extends Activity {
private RotatingImageView rotatingImageView = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
rotatingImageView = (RotatingImageView) findViewById(
R.id.rotatingImageView);
rotatingImageView.startRotation();
}
@Override
protected void onPause() {
super.onPause();
rotatingImageView.stopRotation();
}
@Override
protected void onResume() {
super.onResume();
rotatingImageView.startRotation();
}
}
RotatingImageView.java (custom ImageView):
package com.example.rotatingimageviewapp;
import java.util.Timer;
import java.util.TimerTask;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;
import android.widget.ImageView;
public class RotatingImageView extends ImageView {
private static final long ANIMATION_PERIOD_MS = 1000 / 24;
//The Handler that does the rotation animation
private final Handler handler = new Handler() {
private float currentAngle = 0f;
private final Object animLock = new Object();
private RotateAnimation anim = null;
@Override
public void handleMessage(Message msg) {
float nextAngle = 360 - msg.getData().getFloat("rotation");
synchronized (animLock) {
anim = new RotateAnimation(
currentAngle,
nextAngle,
Animation.RELATIVE_TO_SELF,
.5f,
Animation.RELATIVE_TO_SELF,
.5f);
anim.setDuration(ANIMATION_PERIOD_MS);
/**
* Commenting out the following line allows getActivity() to
* return immediately!
*/
startAnimation(anim);
}
currentAngle = nextAngle;
}
};
private float rotation = 0f;
private final Timer timer = new Timer(true);
private TimerTask timerTask = null;
public RotatingImageView(Context context) {
super(context);
}
public RotatingImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public RotatingImageView(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
}
public void startRotation() {
stopRotation();
/**
* Set up the task that calculates the rotation value
* and tells the Handler to do the rotation
*/
timerTask = new TimerTask() {
@Override
public void run() {
//Calculate next rotation value
rotation += 15f;
while (rotation >= 360f) {
rotation -= 360f;
}
//Tell the Handler to do the rotation
Bundle bundle = new Bundle();
bundle.putFloat("rotation", rotation);
Message msg = new Message();
msg.setData(bundle);
handler.sendMessage(msg);
}
};
timer.schedule(timerTask, 0, ANIMATION_PERIOD_MS);
}
public void stopRotation() {
if (null != timerTask) {
timerTask.cancel();
}
}
}
MainActivityTest.java:
package com.example.rotatingimageviewapp.test;
import android.app.Activity;
import android.test.ActivityInstrumentationTestCase2;
import com.example.rotatingimageviewapp.MainActivity;
public class MainActivityTest extends
ActivityInstrumentationTestCase2<MainActivity> {
public MainActivityTest() {
super(MainActivity.class);
}
protected void setUp() throws Exception {
super.setUp();
}
protected void tearDown() throws Exception {
super.tearDown();
}
public void test001() {
assertEquals(1 + 2, 3 + 0);
}
public void test002() {
//Test hangs on the following line until app goes to background
Activity activity = getActivity();
assertNotNull(activity);
}
public void test003() {
assertEquals(1 + 2, 3 + 0);
}
}
not sure if you guys solve this. But this is my solution, just override method getActivity():
@Override
public MyActivity getActivity() {
if (mActivity == null) {
Intent intent = new Intent(getInstrumentation().getTargetContext(), MyActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// register activity that need to be monitored.
monitor = getInstrumentation().addMonitor(MyActivity.class.getName(), null, false);
getInstrumentation().getTargetContext().startActivity(intent);
mActivity = (MyActivity) getInstrumentation().waitForMonitor(monitor);
setActivity(mActivity);
}
return mActivity;
}
I can tell you why this is happening and have a slight workaround, i think you should be able to do something with your view but this should work for now.
The problem is, when you call getActivity() it goes through a series of methods until it hits the following in InstrumentationTestCase.java
public final <T extends Activity> T launchActivityWithIntent(
String pkg,
Class<T> activityCls,
Intent intent) {
intent.setClassName(pkg, activityCls.getName());
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
T activity = (T) getInstrumentation().startActivitySync(intent);
getInstrumentation().waitForIdleSync();
return activity;
}
The issue is the pesky line that has the following:
getInstrumentation().waitForIdleSync();
Because of your animation there is never an idle on the main thread and so it never returns from this method! how can you fix this? well its fairly easy you will have to override this method so it no longer has that line in. You may have to add in some code to put a wait in to make sure the activity is launched though otherwise this method will return too quickly! I suggest waiting for a view specific to this activity.
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