ConditionWatcher icon indicating copy to clipboard operation
ConditionWatcher copied to clipboard

Make it easy to get the latest activity

Open rfreitas opened this issue 8 years ago • 4 comments

First, thanks for this tool! IdlingResource is a total mess.

Now to business, from the documentation I was not able to understand how to get the latest activity.

However, I found a different way, which follows:

    //ref: http://stackoverflow.com/a/38990078/689223
    static private Activity getCurrentActivity(){
        Collection<Activity> resumedActivity = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED);

        for(Activity act : resumedActivity){
            return act;
        }
        return null;
    }

    static private Activity getCurrentActivitySafe(){
        Callable<Activity> callable = new Callable<Activity>() {
            @Override
            public Activity call() throws Exception {
                return getCurrentActivity();
            }
        };

        FutureTask<Activity> task = new FutureTask<>(callable);
        getInstrumentation().runOnMainSync(task);

        try {
            return task.get(); // Blocks
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

Example on how to use:

@Override
public boolean checkCondition() {
     Activity currentActivity = getCurrentActivitySafe();
    (...)
}

getCurrentActivitySafe() has to be called instead getCurrentActivity() because checkCondition can run outside of the UI thread, causing an error.

My suggestion is to make getCurrentActivitySafe() an utility function, or to rename it to getCurrentActivity() and make it a method of Instruction. Another alternative is to make checkCondition a method that receives the current activity as an argument, like this checkCondition(Activity currentActivity)

rfreitas avatar Mar 06 '17 18:03 rfreitas

Hello :) Thank you for enjoying simple thing as ConditionWatcher. Your proposition about retrieving Activity in test thread is interesting. It's simply waiting for ActivityLifecycleMonitor to return non-null instance.

You said that you don't get how I am doing it. Well my solution is to write your own runner and extend Application object used by your app under test.

public class MyAppTestRunner extends AndroidJUnitRunner {

    @Override
    public Application newApplication(ClassLoader cl, String className, Context context)
            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        return super.newApplication(cl, MyAppTestApplication.class.getName(), context);
    }
}

So now my app will use MyAppTestApplication instead of MyAppApplication. And in MyAppTestApplication I use interface of ActivityLifecycleCallbacks. Whenever any Activity will call onResume the callback will trigger and save new reference to Activity. I can get access to it anytime.

public class MyAppTestApplication extends MyAppApplication implements Application.ActivityLifecycleCallbacks {

    private Activity currentActivity;

    @Override
    public void onCreate() {
        super.onCreate();
        registerActivityLifecycleCallbacks(this);
    }

    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
   
    }

    @Override
    public void onActivityStarted(Activity activity) {

    }

    @Override
    public void onActivityResumed(Activity activity) {
        currentActivity = activity;
    }

    @Override
    public void onActivityPaused(Activity activity) {

    }

    @Override
    public void onActivityStopped(Activity activity) {

    }

    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {

    }

    @Override
    public void onActivityDestroyed(Activity activity) {
    }

    public Activity getCurrentActivity() {
        return currentActivity;
    }

    public static MyAppTestApplication getInstance(){
        return ((MyAppTestApplication)(InstrumentationRegistry.getTargetContext().getApplicationContext()));
    }
}

And with that you can do static imports of MyAppTestApplication.getInstance().getCurrentActivity() which will allow you to simply call getCurrentActivity() wherever you want. I agree with the fact that it can be null - but since last year I never had problem with this as I am aware of it and I simply use null checks :) Well. I think I like your idea - it is a little bit more complicated but takes care of your null checks. I would like to run it with my code for a while and see if there are no any problems/side effects.

What do you think about memory leak threat in both cases?

FisherKK avatar Mar 06 '17 20:03 FisherKK

Didn't know about that solution, so thanks for the share ;) you should put it in the docs! :)

Between the two solutions I don't know which one is more is more reliable, I would have to go deeper into it to find out. The one you proposed is a bit too verbose, though.

Nonetheless, I don't mind so much which solution gets picked, what I do think is important is to make it easy for the user to get the current activity. So whatever solution we go with, it should be wrapped in a function and exposed as an utility function (or passed as an argument, if it's commonly used).

Memory leaks wise it looks to me that both solution are the same, the getCurrentActivitySafe might be a bit safer, since it holds no references.

rfreitas avatar Mar 06 '17 21:03 rfreitas

Did your solution work for you?

For me it doesn't seems to block anything and I receive massive amount of null pointers from getCurrentActivity() in Instructions.

FisherKK avatar Mar 09 '17 12:03 FisherKK

It did, but I made a slight alteration.

static public void performWhenVisible(final int viewId, ViewAction... viewActions) {
        try {
            ConditionWatcher.waitForCondition(new ViewVisibleInstruction(viewId));
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }

        ViewInteraction viewInteraction = onView(allOf(withId(viewId)));
        viewInteraction.perform(viewActions);
    }

    static public class ViewVisibleInstruction extends SafeInstruction {
        static final String TAG = ViewVisibleInstruction.class.getName();
        int mViewId;

        ViewVisibleInstruction(int viewId) {
            mViewId = viewId;
        }

        @Override
        public String getDescription() {
            return TAG;
        }

        @Override
        public boolean checkConditionSafe() {
            Log.d(TAG, "isIdleNow");
            Activity currentActivity = getCurrentActivity();
            Log.d(TAG, "currentActivity:"+currentActivity);
            if (currentActivity==null) return false;

            View intendedView = currentActivity.findViewById(mViewId);
            Log.d(TAG, "intendedView:"+intendedView);
            boolean isVisible = intendedView!=null && intendedView.isShown();
            Log.d(TAG,"checkCondition:"+isVisible);
            return isVisible;
        }
    }

    static abstract public class SafeInstruction extends Instruction {
        static final String TAG = SafeInstruction.class.getName();
        @Override
        public boolean checkCondition() {
            Callable<Boolean> callable = new Callable<Boolean>() {
                @Override
                public Boolean call() throws Exception {
                    return checkConditionSafe();
                }
            };

            FutureTask<Boolean> task = new FutureTask<>(callable);
            getInstrumentation().runOnMainSync(task);

            try {
                return task.get(); // Blocks
            } catch (Exception e) {
                Log.e(TAG, "checkConditionSafe FutureTask exception", e);
                throw new RuntimeException(e);
            }
        }

        abstract public boolean checkConditionSafe();
    }


    //ref: http://stackoverflow.com/a/38990078/689223
    static private Activity getCurrentActivity(){
        Collection<Activity> resumedActivity = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED);

        for(Activity act : resumedActivity){
            return act;
        }
        return null;
    }

rfreitas avatar Apr 08 '17 11:04 rfreitas