blob: 985c5d5ecb9a79e02cb8ae4ddd853d38e79005f5 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.base.test.transit;
import static org.junit.Assert.fail;
import androidx.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
/**
* Base class for states with conditions for entering and exiting them.
*
* <p>Conditions include the existence of {@link Elements}, e.g. Views.
*
* <pre>ConditionalStates can be in the following phases:
* - NEW: Inactive, just created. No transition has started.
* - TRANSITIONING_TO: A transition into the state has started, but enter conditions might not be
* fulfilled yet.
* - ACTIVE: Active, declared elements should exist.
* - TRANSITIONING_FROM: A transition out of the state has started, but exit conditions are not
* fulfilled yet.
* - FINISHED: Inactive, transition away is done.
* </pre>
*
* <p>The lifecycle of ConditionalStates is linear:
*
* <p>NEW > TRANSITIONING_TO > ACTIVE > TRANSITIONING_FROM > FINISHED
*
* <p>Once FINISHED, the ConditionalState does not change state anymore.
*
* <p>This is the base class for {@link TransitStation} and {@link StationFacility}.
*/
public abstract class ConditionalState {
@Phase private int mLifecyclePhase = Phase.NEW;
private Elements mElements;
/** Lifecycle phases of ConditionalState. */
@IntDef({
Phase.NEW,
Phase.TRANSITIONING_TO,
Phase.ACTIVE,
Phase.TRANSITIONING_FROM,
Phase.FINISHED
})
@Retention(RetentionPolicy.SOURCE)
public @interface Phase {
int NEW = 0;
int TRANSITIONING_TO = 1;
int ACTIVE = 2;
int TRANSITIONING_FROM = 3;
int FINISHED = 4;
}
/**
* Declare the {@link Elements} that define this ConditionalState, such as Views.
*
* <p>Transit-layer {@link TransitStation}s and {@link StationFacility}s should override this
* and use the |elements| param to declare what elements need to be waited for for the state to
* be considered active.
*
* @param elements use the #declare___() methods to describe the Elements that define the state.
*/
public abstract void declareElements(Elements.Builder elements);
List<Condition> getEnterConditions() {
initElements();
return mElements.getEnterConditions();
}
List<Condition> getExitConditions() {
initElements();
return mElements.getExitConditions();
}
private void initElements() {
if (mElements == null) {
Elements.Builder builder = new Elements.Builder();
declareElements(builder);
mElements = builder.build(this);
}
}
void setStateTransitioningTo() {
assertInPhase(Phase.NEW);
mLifecyclePhase = Phase.TRANSITIONING_TO;
onStartMonitoringTransitionTo();
for (Condition condition : getEnterConditions()) {
condition.onStartMonitoring();
}
}
/** Hook to setup observers for the transition into the ConditionalState. */
protected void onStartMonitoringTransitionTo() {}
void setStateActive() {
assertInPhase(Phase.TRANSITIONING_TO);
mLifecyclePhase = Phase.ACTIVE;
onStopMonitoringTransitionTo();
}
/** Hook to cleanup observers for the transition into the ConditionalState. */
protected void onStopMonitoringTransitionTo() {}
void setStateTransitioningFrom() {
assertInPhase(Phase.ACTIVE);
mLifecyclePhase = Phase.TRANSITIONING_FROM;
onStartMonitoringTransitionFrom();
for (Condition condition : getExitConditions()) {
condition.onStartMonitoring();
}
}
/** Hook to setup observers for the transition from the ConditionalState. */
protected void onStartMonitoringTransitionFrom() {}
void setStateFinished() {
assertInPhase(Phase.TRANSITIONING_FROM);
mLifecyclePhase = Phase.FINISHED;
onStopMonitoringTransitionFrom();
}
/** Hook to cleanup observers for the transition from the ConditionalState. */
protected void onStopMonitoringTransitionFrom() {}
/**
* @return the lifecycle {@link Phase} this ConditionalState is in.
*/
public @Phase int getPhase() {
return mLifecyclePhase;
}
/** Assert this ConditionalState is in an expected lifecycle {@link Phase}. */
public void assertInPhase(@Phase int expectedPhase) {
if (mLifecyclePhase != expectedPhase) {
fail(
String.format(
"%s should have been in %s, but was %s",
this, phaseToString(expectedPhase), phaseToString(mLifecyclePhase)));
}
}
/** Check the enter Conditions are still fulfilled. */
public final void recheckEnterConditions() {
assertInPhase(Phase.ACTIVE);
ConditionChecker.check(getEnterConditions());
}
/**
* @return a String representation of a lifecycle {@link Phase}.
*/
public static String phaseToString(@Phase int phase) {
switch (phase) {
case Phase.NEW:
return "Phase.NEW";
case Phase.TRANSITIONING_TO:
return "Phase.TRANSITIONING_TO";
case Phase.ACTIVE:
return "Phase.ACTIVE";
case Phase.TRANSITIONING_FROM:
return "Phase.TRANSITIONING_AWAY";
case Phase.FINISHED:
return "Phase.FINISHED";
default:
throw new IllegalArgumentException("No string representation for phase " + phase);
}
}
public static String phaseToShortString(@Phase int phase) {
switch (phase) {
case Phase.NEW:
return "NEW";
case Phase.TRANSITIONING_TO:
return "TRANSITIONING_TO";
case Phase.ACTIVE:
return "ACTIVE";
case Phase.TRANSITIONING_FROM:
return "TRANSITIONING_AWAY";
case Phase.FINISHED:
return "FINISHED";
default:
throw new IllegalArgumentException(
"No short string representation for phase " + phase);
}
}
}