SessionController.java

/*
 * Copyright 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.car.app.testing;

import static java.util.Objects.requireNonNull;

import android.content.Intent;

import androidx.annotation.NonNull;
import androidx.car.app.Session;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.Lifecycle.Event;
import androidx.lifecycle.LifecycleRegistry;

import java.lang.reflect.Field;

/**
 * A controller that allows testing of a {@link Session}.
 *
 * <p>This controller allows:
 *
 * <ul>
 *   <li>Injecting a {@link TestCarContext} into the {@link Session} instance, which provides access
 *   to the test managers and other testing functionalities.
 * </ul>
 */
@SuppressWarnings("NotCloseable")
public class SessionController {
    private final Session mSession;
    private final TestCarContext mTestCarContext;

    /**
     * Creates a {@link SessionController} to control the provided {@link Session}.
     *
     * @param session the {@link Session} to control
     * @param context the {@link TestCarContext} that the {@code session} should use.
     * @throws NullPointerException if {@code session} or {@code context} is {@code null}
     */
    @NonNull
    public static SessionController of(@NonNull Session session, @NonNull TestCarContext context) {
        return new SessionController(requireNonNull(session), requireNonNull(context));
    }

    /**
     * Creates the {@link Session} that is being controlled with the given {@code intent}.
     *
     * <p>If this is the first time this is called on the {@link Session}, this would trigger
     * {@link Session#onCreateScreen(Intent)} and transition the lifecycle to the
     * {@link Lifecycle.State#CREATED} state. Otherwise, this will trigger
     * {@link Session#onNewIntent(Intent)}.
     *
     * @see Session#getLifecycle
     */
    @NonNull
    public SessionController create(@NonNull Intent intent) {
        LifecycleRegistry registry = (LifecycleRegistry) mSession.getLifecycle();
        Lifecycle.State state = registry.getCurrentState();
        TestScreenManager screenManager = mTestCarContext.getCarService(TestScreenManager.class);

        int screenStackSize = screenManager.getScreensPushed().size();
        if (!state.isAtLeast(Lifecycle.State.CREATED) || screenStackSize < 1) {
            registry.handleLifecycleEvent(Event.ON_CREATE);
            screenManager.push(mSession.onCreateScreen(intent));
        } else {
            mSession.onNewIntent(intent);
        }

        return this;
    }

    /**
     * Starts the {@link Session} that is being controlled.
     *
     * @see Session#getLifecycle
     */
    @NonNull
    public SessionController start() {
        LifecycleRegistry registry = (LifecycleRegistry) mSession.getLifecycle();
        registry.handleLifecycleEvent(Event.ON_START);

        return this;
    }


    /**
     * Resumes the {@link Session} that is being controlled.
     *
     * @see Session#getLifecycle
     */
    @NonNull
    public SessionController resume() {
        LifecycleRegistry registry = (LifecycleRegistry) mSession.getLifecycle();
        registry.handleLifecycleEvent(Event.ON_RESUME);

        return this;
    }

    /**
     * Pauses the {@link Session} that is being controlled.
     *
     * @see Session#getLifecycle
     */
    @NonNull
    public SessionController pause() {
        LifecycleRegistry registry = (LifecycleRegistry) mSession.getLifecycle();
        registry.handleLifecycleEvent(Event.ON_PAUSE);

        return this;
    }

    /**
     * Stops the {@link Session} that is being controlled.
     *
     * @see Session#getLifecycle
     */
    @NonNull
    public SessionController stop() {
        LifecycleRegistry registry = (LifecycleRegistry) mSession.getLifecycle();
        registry.handleLifecycleEvent(Event.ON_STOP);

        return this;
    }

    /**
     * Destroys the {@link Session} that is being controlled.
     *
     * @see Session#getLifecycle
     */
    @NonNull
    public SessionController destroy() {
        LifecycleRegistry registry = (LifecycleRegistry) mSession.getLifecycle();
        registry.handleLifecycleEvent(Event.ON_DESTROY);

        return this;
    }

    /** Returns the {@link Session} that is being controlled. */
    @NonNull
    public Session get() {
        return mSession;
    }

    private SessionController(Session session, TestCarContext context) {
        mSession = session;
        mTestCarContext = context;

        // Use reflection to inject the TestCarContext into the Session.
        try {
            Field registry = Session.class.getDeclaredField("mRegistry");
            registry.setAccessible(true);
            registry.set(session, mTestCarContext.getLifecycleOwner().mRegistry);

            Field carContext = Session.class.getDeclaredField("mCarContext");
            carContext.setAccessible(true);
            carContext.set(session, mTestCarContext);
        } catch (ReflectiveOperationException e) {
            throw new IllegalStateException(
                    "Failed to set internal Session values for testing", e);
        }
    }
}