1 // Copyright 2020 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.base.test.util; 6 7 import android.app.Activity; 8 9 import androidx.test.runner.lifecycle.ActivityLifecycleCallback; 10 import androidx.test.runner.lifecycle.ActivityLifecycleMonitor; 11 import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry; 12 import androidx.test.runner.lifecycle.Stage; 13 14 import org.junit.Assert; 15 16 import org.chromium.base.ThreadUtils; 17 18 import java.util.concurrent.TimeUnit; 19 import java.util.concurrent.TimeoutException; 20 import java.util.concurrent.atomic.AtomicReference; 21 22 /** Methods used for testing Application-level behavior. */ 23 public class ApplicationTestUtils { 24 private static final ActivityLifecycleMonitor sMonitor = 25 ActivityLifecycleMonitorRegistry.getInstance(); 26 27 private static final long ACTIVITY_TIMEOUT = 10000; 28 29 /** Waits until the given activity transitions to the given state. */ waitForActivityState(Activity activity, Stage stage)30 public static void waitForActivityState(Activity activity, Stage stage) { 31 waitForActivityState( 32 "Activity " 33 + activity.getLocalClassName() 34 + " did not reach stage: " 35 + stage 36 + ". Is the device screen turned on?", 37 activity, 38 stage); 39 } 40 41 /** Waits until the given activity transitions to the given state. */ waitForActivityState(String failureReason, Activity activity, Stage stage)42 public static void waitForActivityState(String failureReason, Activity activity, Stage stage) { 43 CriteriaHelper.pollUiThread( 44 () -> { 45 return sMonitor.getLifecycleStageOf(activity) == stage; 46 }, 47 failureReason, 48 ACTIVITY_TIMEOUT, 49 CriteriaHelper.DEFAULT_POLLING_INTERVAL); 50 // De-flake by flushing the tasks that are already queued on the Looper's Handler. 51 // TODO(https://crbug.com/1424788): Remove this and properly fix flaky tests. 52 TestThreadUtils.flushNonDelayedLooperTasks(); 53 } 54 55 /** Finishes the given activity and waits for its onDestroy() to be called. */ finishActivity(final Activity activity)56 public static void finishActivity(final Activity activity) throws Exception { 57 ThreadUtils.runOnUiThreadBlocking( 58 () -> { 59 if (sMonitor.getLifecycleStageOf(activity) != Stage.DESTROYED) { 60 activity.finish(); 61 } 62 }); 63 final String error = 64 "Failed to finish the Activity. Did you start a second Activity and " 65 + "not finish it?"; 66 waitForActivityState(error, activity, Stage.DESTROYED); 67 } 68 69 /** 70 * Recreates the provided Activity, returning the newly created Activity once it's finished 71 * starting up. 72 * @param activity The Activity to recreate. 73 * @return The newly created Activity. 74 */ recreateActivity(T activity)75 public static <T extends Activity> T recreateActivity(T activity) { 76 return waitForActivityWithClass( 77 activity.getClass(), Stage.RESUMED, () -> activity.recreate()); 78 } 79 80 /** 81 * Waits for an activity of the specified class to reach the specified Activity {@link Stage}, 82 * triggered by running the provided trigger. 83 * 84 * @param activityClass The class type to wait for. 85 * @param state The Activity {@link Stage} to wait for an activity of the right class type to 86 * reach. 87 * @param uiThreadTrigger The Runnable that will trigger the state change to wait for. The 88 * Runnable will be run on the UI thread 89 */ waitForActivityWithClass( Class<? extends Activity> activityClass, Stage stage, Runnable uiThreadTrigger)90 public static <T extends Activity> T waitForActivityWithClass( 91 Class<? extends Activity> activityClass, Stage stage, Runnable uiThreadTrigger) { 92 return waitForActivityWithClass(activityClass, stage, uiThreadTrigger, null); 93 } 94 95 /** 96 * Waits for an activity of the specified class to reach the specified Activity {@link Stage}, 97 * triggered by running the provided trigger. 98 * 99 * @param activityClass The class type to wait for. 100 * @param state The Activity {@link Stage} to wait for an activity of the right class type to 101 * reach. 102 * @param uiThreadTrigger The Runnable that will trigger the state change to wait for, which 103 * will be run on the UI thread. 104 * @param backgroundThreadTrigger The Runnable that will trigger the state change to wait for, 105 * which will be run on the UI thread. 106 */ waitForActivityWithClass( Class<? extends Activity> activityClass, Stage stage, Runnable uiThreadTrigger, Runnable backgroundThreadTrigger)107 public static <T extends Activity> T waitForActivityWithClass( 108 Class<? extends Activity> activityClass, 109 Stage stage, 110 Runnable uiThreadTrigger, 111 Runnable backgroundThreadTrigger) { 112 ThreadUtils.assertOnBackgroundThread(); 113 final CallbackHelper activityCallback = new CallbackHelper(); 114 final AtomicReference<T> activityRef = new AtomicReference<>(); 115 ActivityLifecycleCallback stateListener = 116 (Activity newActivity, Stage newStage) -> { 117 if (newStage == stage) { 118 if (!activityClass.isAssignableFrom(newActivity.getClass())) return; 119 120 activityRef.set((T) newActivity); 121 ThreadUtils.postOnUiThread(() -> activityCallback.notifyCalled()); 122 } 123 }; 124 sMonitor.addLifecycleCallback(stateListener); 125 126 try { 127 if (uiThreadTrigger != null) { 128 ThreadUtils.runOnUiThreadBlocking(() -> uiThreadTrigger.run()); 129 } 130 if (backgroundThreadTrigger != null) backgroundThreadTrigger.run(); 131 activityCallback.waitForFirst( 132 "No Activity reached target state.", ACTIVITY_TIMEOUT, TimeUnit.MILLISECONDS); 133 T createdActivity = activityRef.get(); 134 Assert.assertNotNull("Activity reference is null.", createdActivity); 135 return createdActivity; 136 } catch (TimeoutException e) { 137 throw new RuntimeException(e); 138 } finally { 139 sMonitor.removeLifecycleCallback(stateListener); 140 } 141 } 142 } 143