5

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);
    }

}
Gary Sheppard
  • 4,764
  • 3
  • 25
  • 35

5 Answers5

10

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;
    }
nebula
  • 382
  • 3
  • 10
1

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.

Paul Harris
  • 5,769
  • 1
  • 25
  • 41
  • Thanks for taking a look, Paul! I appreciate the insight. I think knowing about this `waitForIdleSync()` call gets me closer to a fix. That would make it the same issue as [this (unanswered) question](http://stackoverflow.com/questions/20151699/android-infinitely-repeating-animation-vs-instrumentation-waitforidlesync). However, `launchActivityWithIntent` is a `final` method and hence cannot be overridden, so I'm not sure how to implement the workaround you suggest. But as I said, I appreciate the info. – Gary Sheppard Dec 31 '13 at 21:05
  • 1
    You can just copy the method with a different name in to your test class and the same for the getActivity() method and then use that rather than overload it specifically. I suggest putting it into a class that extends whatever you are currently using so you can use it in all your tests – Paul Harris Jan 01 '14 at 05:10
  • okay, now I get it. Thanks for clarifying. I implemented it, and unfortunately, the blocking call is actually `startActivitySync(intent)`, before I even get to `waitForIdleSync()`. Inside `startActivitySync`, the blocking call is `mSync.wait()`...ugly threading stuff, and I guess I need to dig into this more to figure it out. I can't skip `startActivitySync` or else I can't start the activity to be tested. Please let me know if you have more ideas, though you have already helped a great deal. – Gary Sheppard Jan 02 '14 at 14:20
  • 1
    You can try doing getTargetContext().startActivity(intent); where the intent is the one to launch your activity, it should work with the same caveats i gave before but it does imply something weird is going on, can you post your activiys code or a subset? – Paul Harris Jan 02 '14 at 14:28
  • Thanks, but `startActivity` doesn't return the actual activity. I did post the activity code in the original question above. However, I have found a different way to rotate the `ImageView` without using `Animation`, which doesn't block the call to `getActivity`. I will write it up in a separate answer. If you (or anyone) posts a [SSCCE](http://www.sscce.org/) that works and answers this question, I'll gladly mark it as the accepted answer. Thanks again! – Gary Sheppard Jan 02 '14 at 15:40
  • Paul, any idea, on what condition, 'getInstrumentation().waitForIdleSync();' would get into infinite loop? In android 4.4.2_r2 running processor board, I'm facing this issue, while executing the CTS Test. – ArunJTS Aug 15 '14 at 08:10
  • I do not know exactly what is going on but it is probably related to work being done on the main thread and it constantly waiting for it to become idle, typically you can replace waitForIdleSync() with a more explicit wait, such as waiting for a specific gui element to appear using your normal search methods. If you give more detail I might be able to help. – Paul Harris Aug 15 '14 at 09:09
  • @Paul , Please refer [query_1](http://stackoverflow.com/questions/25278712/waitforidlesync-improper-behaviour) and [query_1](http://stackoverflow.com/questions/25242679/android-cts-progress-dialog-instrumentation-issue) for **'waitForIdleSync();'** (CTS Test code) get into hanging in processor board running with Android kitkat 4.4.42_r2. – ArunJTS Aug 16 '14 at 03:14
  • @ArunJTS These links are no more available – dragi Jan 23 '15 at 16:32
0

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.

Community
  • 1
  • 1
Gary Sheppard
  • 4,764
  • 3
  • 25
  • 35
0

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();
    }

}
Nikolay Mihaylov
  • 3,868
  • 8
  • 27
  • 32
Jacard
  • 1
0

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.

Tom Rutchik
  • 1,183
  • 1
  • 12
  • 15