问题
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);
}
}
回答1:
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;
}
回答2:
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.
回答3:
UPDATE: thanks to @nebula for the answer above: https://stackoverflow.com/a/24506584/720773
I learned about an easy workaround for this issue: use a different approach to rotate the image, which does not involve Animation:
Android: Rotate image in imageview by an angle
That doesn't really answer my question, but it works around the issue. If anyone knows how to get ActivityInstrumentationTestCase2.getActivity() to return the Activity while using the Animation class in a custom ImageView, please post a SSCCE as an answer and I'll accept it instead of this one if it works.
回答4:
I learned about every workaround for this issue,and this is my solution,it works well, thx everybody ;)
public class TestApk extends ActivityInstrumentationTestCase2 {
private static final String LAUNCHER_ACTIVITY_FULL_CLASSNAME =
"com.notepad.MainActivity";
private static Class launcherActivityClass;
static {
try {
launcherActivityClass = Class
.forName(LAUNCHER_ACTIVITY_FULL_CLASSNAME);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
public TestApk () throws ClassNotFoundException {
super(launcherActivityClass);
}
private Solo solo;
@Override
protected void setUp() throws Exception {
solo = new Solo(getInstrumentation());
Intent intent = new Intent(getInstrumentation().getTargetContext(), launcherActivityClass);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
getInstrumentation().getTargetContext().startActivity(intent);
}
public void test_ookla_speedtest() {
Boolean expect = solo.waitForText("Login", 0, 60*1000);
assertTrue("xxxxxxxxxxxxxxxxxxx", expect);
}
@Override
public void tearDown() throws Exception {
solo.finishOpenedActivities();
super.tearDown();
}
}
回答5:
I believe Paul Harris correctly answered the reason why this problem was happening. So how can you more easily work around this problem? The answer is simple, don't start the animation if you are in test mode. So how can you tell if your in test mode? There's several ways to do that, but one simple way to do that is to add some extra data to the intent you used to start the activity in you test. I'll give example code in terms of using AndroidJUnit (My understanding is that ActivityInstrumentationTestCase2 is deprecated, or at least AndroidJUnit is the new way to do Instrumented tests; and I'm also assuming AndroidJUnit also does that call to waitForIdleSync, which I've not verified)
@Rule
public ActivityTestRule<MainActivity> mActivityRule =
new ActivityTestRule<>(MainActivity.class, true, false);
@Before
public init() {
Activity mActivity;
Intent intent = new Intent();
intent.put("isTestMode, true);
mActivity = mActivityRule.launchActivity(intent);
}
So in your MainActivity onCreate method, do the following:
Boolean isTestMode = (Boolean)savedInstanceState.get("isTestMode");
if (isTestMode == null || !isTestMode) {
rotatingImageView.startRotation();
}
After the activity is launched you can use some other means to startRotation if that's important to you.
来源:https://stackoverflow.com/questions/20860832/why-does-getactivity-block-during-junit-test-when-custom-imageview-calls-start