1 // Copyright 2023 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.transit; 6 7 import static org.junit.Assert.fail; 8 9 import androidx.annotation.IntDef; 10 11 import java.lang.annotation.Retention; 12 import java.lang.annotation.RetentionPolicy; 13 import java.util.List; 14 15 /** 16 * Base class for states with conditions for entering and exiting them. 17 * 18 * <p>Conditions include the existence of {@link Elements}, e.g. Views. 19 * 20 * <pre>ConditionalStates can be in the following phases: 21 * - NEW: Inactive, just created. No transition has started. 22 * - TRANSITIONING_TO: A transition into the state has started, but enter conditions might not be 23 * fulfilled yet. 24 * - ACTIVE: Active, declared elements should exist. 25 * - TRANSITIONING_FROM: A transition out of the state has started, but exit conditions are not 26 * fulfilled yet. 27 * - FINISHED: Inactive, transition away is done. 28 * </pre> 29 * 30 * <p>The lifecycle of ConditionalStates is linear: 31 * 32 * <p>NEW > TRANSITIONING_TO > ACTIVE > TRANSITIONING_FROM > FINISHED 33 * 34 * <p>Once FINISHED, the ConditionalState does not change state anymore. 35 * 36 * <p>This is the base class for {@link TransitStation} and {@link StationFacility}. 37 */ 38 public abstract class ConditionalState { 39 @Phase private int mLifecyclePhase = Phase.NEW; 40 private Elements mElements; 41 42 /** Lifecycle phases of ConditionalState. */ 43 @IntDef({ 44 Phase.NEW, 45 Phase.TRANSITIONING_TO, 46 Phase.ACTIVE, 47 Phase.TRANSITIONING_FROM, 48 Phase.FINISHED 49 }) 50 @Retention(RetentionPolicy.SOURCE) 51 public @interface Phase { 52 int NEW = 0; 53 int TRANSITIONING_TO = 1; 54 int ACTIVE = 2; 55 int TRANSITIONING_FROM = 3; 56 int FINISHED = 4; 57 } 58 59 /** 60 * Declare the {@link Elements} that define this ConditionalState, such as Views. 61 * 62 * <p>Transit-layer {@link TransitStation}s and {@link StationFacility}s should override this 63 * and use the |elements| param to declare what elements need to be waited for for the state to 64 * be considered active. 65 * 66 * @param elements use the #declare___() methods to describe the Elements that define the state. 67 */ declareElements(Elements.Builder elements)68 public abstract void declareElements(Elements.Builder elements); 69 getEnterConditions()70 List<Condition> getEnterConditions() { 71 initElements(); 72 return mElements.getEnterConditions(); 73 } 74 getExitConditions()75 List<Condition> getExitConditions() { 76 initElements(); 77 return mElements.getExitConditions(); 78 } 79 initElements()80 private void initElements() { 81 if (mElements == null) { 82 Elements.Builder builder = new Elements.Builder(); 83 declareElements(builder); 84 mElements = builder.build(this); 85 } 86 } 87 setStateTransitioningTo()88 void setStateTransitioningTo() { 89 assertInPhase(Phase.NEW); 90 mLifecyclePhase = Phase.TRANSITIONING_TO; 91 onStartMonitoringTransitionTo(); 92 for (Condition condition : getEnterConditions()) { 93 condition.onStartMonitoring(); 94 } 95 } 96 97 /** Hook to setup observers for the transition into the ConditionalState. */ onStartMonitoringTransitionTo()98 protected void onStartMonitoringTransitionTo() {} 99 setStateActive()100 void setStateActive() { 101 assertInPhase(Phase.TRANSITIONING_TO); 102 mLifecyclePhase = Phase.ACTIVE; 103 onStopMonitoringTransitionTo(); 104 } 105 106 /** Hook to cleanup observers for the transition into the ConditionalState. */ onStopMonitoringTransitionTo()107 protected void onStopMonitoringTransitionTo() {} 108 setStateTransitioningFrom()109 void setStateTransitioningFrom() { 110 assertInPhase(Phase.ACTIVE); 111 mLifecyclePhase = Phase.TRANSITIONING_FROM; 112 onStartMonitoringTransitionFrom(); 113 for (Condition condition : getExitConditions()) { 114 condition.onStartMonitoring(); 115 } 116 } 117 118 /** Hook to setup observers for the transition from the ConditionalState. */ onStartMonitoringTransitionFrom()119 protected void onStartMonitoringTransitionFrom() {} 120 setStateFinished()121 void setStateFinished() { 122 assertInPhase(Phase.TRANSITIONING_FROM); 123 mLifecyclePhase = Phase.FINISHED; 124 onStopMonitoringTransitionFrom(); 125 } 126 127 /** Hook to cleanup observers for the transition from the ConditionalState. */ onStopMonitoringTransitionFrom()128 protected void onStopMonitoringTransitionFrom() {} 129 130 /** 131 * @return the lifecycle {@link Phase} this ConditionalState is in. 132 */ getPhase()133 public @Phase int getPhase() { 134 return mLifecyclePhase; 135 } 136 137 /** Assert this ConditionalState is in an expected lifecycle {@link Phase}. */ assertInPhase(@hase int expectedPhase)138 public void assertInPhase(@Phase int expectedPhase) { 139 if (mLifecyclePhase != expectedPhase) { 140 fail( 141 String.format( 142 "%s should have been in %s, but was %s", 143 this, phaseToString(expectedPhase), phaseToString(mLifecyclePhase))); 144 } 145 } 146 147 /** Check the enter Conditions are still fulfilled. */ recheckEnterConditions()148 public final void recheckEnterConditions() { 149 assertInPhase(Phase.ACTIVE); 150 ConditionChecker.check(getEnterConditions()); 151 } 152 153 /** 154 * @return a String representation of a lifecycle {@link Phase}. 155 */ phaseToString(@hase int phase)156 public static String phaseToString(@Phase int phase) { 157 switch (phase) { 158 case Phase.NEW: 159 return "Phase.NEW"; 160 case Phase.TRANSITIONING_TO: 161 return "Phase.TRANSITIONING_TO"; 162 case Phase.ACTIVE: 163 return "Phase.ACTIVE"; 164 case Phase.TRANSITIONING_FROM: 165 return "Phase.TRANSITIONING_AWAY"; 166 case Phase.FINISHED: 167 return "Phase.FINISHED"; 168 default: 169 throw new IllegalArgumentException("No string representation for phase " + phase); 170 } 171 } 172 phaseToShortString(@hase int phase)173 public static String phaseToShortString(@Phase int phase) { 174 switch (phase) { 175 case Phase.NEW: 176 return "NEW"; 177 case Phase.TRANSITIONING_TO: 178 return "TRANSITIONING_TO"; 179 case Phase.ACTIVE: 180 return "ACTIVE"; 181 case Phase.TRANSITIONING_FROM: 182 return "TRANSITIONING_AWAY"; 183 case Phase.FINISHED: 184 return "FINISHED"; 185 default: 186 throw new IllegalArgumentException( 187 "No short string representation for phase " + phase); 188 } 189 } 190 } 191