Почему getActivity() блокируется во время теста JUnit, когда пользовательский ImageView вызывает startAnimation(Animation)?

Я написал приложение для Android, которое отображает пользовательский ImageView, который периодически вращается, используя startAnimation(Animation). Приложение работает нормально, но если я создаю тест JUnit типа ActivityInstrumentationTestCase2 и тест вызывает getActivity(), этот вызов getActivity() никогда не возвращается до тех пор, пока приложение не перейдет в фоновый режим (например, нажата кнопка «Домой» устройства).

Спустя много времени и разочарований я обнаружил, что getActivity() возвращается немедленно, если я закомментирую вызов startAnimation(Animation) в своем пользовательском классе ImageView. Но это противоречит цели моего пользовательского ImageView, потому что мне нужно его анимировать.

Может ли кто-нибудь сказать мне, почему getActivity() блокируется во время моего теста JUnit, но только при использовании startAnimation? Заранее спасибо всем, кто может предложить обходной путь или сказать мне, что я делаю неправильно.

Примечание. Решение должно работать с Android API уровня не ниже 10.

Вот весь исходный код, необходимый для его запуска (поместите любое изображение PNG в res/drawable и назовите его 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>

Основная активность.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 (пользовательский 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);
    }

}

person Gary Sheppard    schedule 31.12.2013    source источник


Ответы (5)


не уверен, что вы, ребята, решите это. Но это мое решение, просто переопределите метод 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;
    }
person nebula    schedule 01.07.2014
comment
Где метод setActivity? - person kunal.c; 21.12.2018

Я могу рассказать вам, почему это происходит, и предложить небольшой обходной путь, я думаю, вы сможете что-то сделать со своим представлением, но пока это должно работать.

Проблема в том, что когда вы вызываете getActivity(), он проходит через серию методов, пока не наткнется на следующее в 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;
    }

Проблема заключается в надоедливой строке, которая имеет следующее:

getInstrumentation().waitForIdleSync();

Из-за вашей анимации основной поток никогда не простаивает, поэтому он никогда не возвращается из этого метода! как вы можете это исправить? ну, это довольно просто, вам придется переопределить этот метод, чтобы в нем больше не было этой строки. Возможно, вам придется добавить некоторый код, чтобы поставить ожидание, чтобы убедиться, что действие запущено, хотя в противном случае этот метод вернется слишком быстро! Я предлагаю дождаться представления, относящегося к этой деятельности.

person Paul Harris    schedule 31.12.2013
comment
Спасибо за внимание, Павел! Я ценю понимание. Я думаю, что знание об этом waitForIdleSync() звонке приблизит меня к исправлению. Это сделает его такой же проблемой, как этот (без ответа) вопрос. Однако launchActivityWithIntent является методом final и, следовательно, не может быть переопределен, поэтому я не уверен, как реализовать предложенный вами обходной путь. Но, как я уже сказал, я ценю информацию. - person Gary Sheppard; 01.01.2014
comment
Вы можете просто скопировать метод с другим именем в свой тестовый класс и то же самое для метода getActivity(), а затем использовать его, а не специально его перегружать. Я предлагаю поместить его в класс, который расширяет все, что вы используете в настоящее время, чтобы вы могли использовать его во всех своих тестах. - person Paul Harris; 01.01.2014
comment
хорошо, теперь я понял. Спасибо за разъяснения. Я реализовал это, и, к сожалению, блокирующий вызов на самом деле startActivitySync(intent), еще до того, как я доберусь до waitForIdleSync(). Внутри startActivitySync вызов блокировки — это mSync.wait()... уродливая многопоточность, и я думаю, мне нужно больше в этом разобраться, чтобы понять это. Я не могу пропустить startActivitySync, иначе я не могу запустить тестируемое действие. Пожалуйста, дайте мне знать, если у вас есть еще идеи, хотя вы уже очень помогли. - person Gary Sheppard; 02.01.2014
comment
Вы можете попробовать выполнить getTargetContext().startActivity(intent); где целью является запуск вашей активности, она должна работать с теми же предостережениями, которые я давал ранее, но это подразумевает, что происходит что-то странное, можете ли вы опубликовать свой код активности или подмножество? - person Paul Harris; 02.01.2014
comment
Спасибо, но startActivity не возвращает фактическую активность. Я разместил код активности в исходном вопросе выше. Однако я нашел другой способ повернуть ImageView без использования Animation, который не блокирует вызов getActivity. Я напишу это в отдельном ответе. Если вы (или кто-либо другой) опубликуете SSCCE, который работает и отвечает на этот вопрос, я с радостью отмечу его как принятый ответ. Еще раз спасибо! - person Gary Sheppard; 02.01.2014
comment
Пол, есть идеи, при каком условии 'getInstrumentation().waitForIdleSync();' попадет в бесконечный цикл? В процессорной плате Android 4.4.2_r2 я столкнулся с этой проблемой при выполнении теста CTS. - person ArunJTS; 15.08.2014
comment
Я не знаю точно, что происходит, но, вероятно, это связано с работой, выполняемой в основном потоке, и он постоянно ожидает, пока он станет бездействующим, обычно вы можете заменить waitForIdleSync() более явным ожиданием, например ожиданием определенный элемент графического интерфейса, который будет отображаться с использованием ваших обычных методов поиска. Если вы дадите более подробную информацию, я мог бы помочь. - person Paul Harris; 15.08.2014
comment
@Paul, пожалуйста, обратитесь к query_1 и query_1 для 'waitForIdleSync();' (тестовый код CTS) зависает на процессорной плате, работающей с Android kitkat 4.4 .42_r2. - person ArunJTS; 16.08.2014
comment
@ArunJTS Эти ссылки больше не доступны - person dragi; 23.01.2015

ОБНОВЛЕНИЕ: спасибо @nebula за ответ выше: https://stackoverflow.com/a/24506584/720773


Я узнал о простом обходном пути для этой проблемы: используйте другой подход для поворота изображения, который не включает Animation:

Android: повернуть изображение в режиме просмотра изображения на угол

Это на самом деле не отвечает на мой вопрос, но решает проблему. Если кто-нибудь знает, как заставить ActivityInstrumentationTestCase2.getActivity() возвращать Activity при использовании класса Animation в пользовательском ImageView, опубликуйте SSCCE в качестве ответа, и я приму его вместо этого, если он сработает.

person Gary Sheppard    schedule 02.01.2014

Я узнал о каждом обходном пути для этой проблемы, и это мое решение, оно работает хорошо, спасибо всем;)

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

}
person Jacard    schedule 17.06.2016

Я считаю, что Пол Харрис правильно ответил на причину возникновения этой проблемы. Итак, как вы можете более легко обойти эту проблему? Ответ прост, не запускайте анимацию, если вы находитесь в тестовом режиме. Итак, как узнать, что вы находитесь в тестовом режиме? Есть несколько способов сделать это, но один простой способ сделать это — добавить некоторые дополнительные данные к намерению, которое вы использовали для запуска действия в тесте. Я приведу пример кода с точки зрения использования AndroidJUnit (насколько я понимаю, ActivityInstrumentationTestCase2 устарел, или, по крайней мере, AndroidJUnit — это новый способ проведения инструментальных тестов, и я также предполагаю, что AndroidJUnit также выполняет этот вызов для waitForIdleSync, который я не проверено)

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

Итак, в вашем методе MainActivity onCreate сделайте следующее:

Boolean isTestMode = (Boolean)savedInstanceState.get("isTestMode");
if (isTestMode == null || !isTestMode) {
    rotatingImageView.startRotation();
}

После запуска активности вы можете использовать другие средства для запуска Rotation, если это важно для вас.

person Tom Rutchik    schedule 19.07.2019