chromium/chrome/android/java/src/org/chromium/chrome/browser/browserservices/SessionDataHolder.java

// Copyright 2019 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.chrome.browser.browserservices;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.SparseArray;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabsSessionToken;

import dagger.Lazy;

import org.chromium.base.Callback;
import org.chromium.chrome.browser.customtabs.CustomTabsConnection;

import javax.inject.Inject;
import javax.inject.Singleton;

/**
 * Holds the currently active {@link SessionHandler} and redirects relevant intents
 * and calls into it. {@link SessionHandler} is an interface owned by the currently
 * focused activity that has a linkage to a third party client app through a session.
 */
@Singleton
public class SessionDataHolder {
    private final Lazy<CustomTabsConnection> mConnection;
    private final SparseArray<SessionData> mTaskIdToSessionData = new SparseArray<>();

    @Nullable private SessionHandler mActiveSessionHandler;

    @Nullable private Callback<CustomTabsSessionToken> mSessionDisconnectCallback;

    @Inject
    public SessionDataHolder(Lazy<CustomTabsConnection> connection) {
        mConnection = connection;
    }

    /** Data associated with a {@link SessionHandler} necessary to pass new intents to it. */
    private static class SessionData {
        public final CustomTabsSessionToken session;

        // Session handlers can reside in Activities of different types, so we need to store the
        // Activity class to be able to route new intents into it.
        public final Class<? extends Activity> activityClass;

        private SessionData(
                CustomTabsSessionToken session, Class<? extends Activity> activityClass) {
            this.session = session;
            this.activityClass = activityClass;
        }
    }

    /**
     * Sets the currently active {@link SessionHandler} in focus.
     * @param sessionHandler {@link SessionHandler} to set.
     */
    public void setActiveHandler(@NonNull SessionHandler sessionHandler) {
        mActiveSessionHandler = sessionHandler;
        CustomTabsSessionToken session = sessionHandler.getSession();
        if (session == null) return;

        mTaskIdToSessionData.append(
                sessionHandler.getTaskId(),
                new SessionData(session, sessionHandler.getActivityClass()));
        ensureSessionCleanUpOnDisconnects();
    }

    /** Notifies that given {@link SessionHandler} no longer has focus. */
    public void removeActiveHandler(SessionHandler sessionHandler) {
        if (mActiveSessionHandler == sessionHandler) {
            mActiveSessionHandler = null;
        } // else this sessionHandler has already been replaced.

        // Intentionally not removing from mTaskIdToSessionData to handle cases when the task is
        // brought to foreground by a new intent - the CCT might not be able to call
        // setActiveHandler in time.
    }

    /**
     * Returns the class of Activity with a matching session running in the same task as the given
     * intent is being launched from, or null if no such Activity present.
     */
    @Nullable
    public Class<? extends Activity> getActiveHandlerClassInCurrentTask(
            Intent intent, Context context) {
        if (!(context instanceof Activity)) return null;
        int taskId = ((Activity) context).getTaskId();
        SessionData handlerDataInCurrentTask = mTaskIdToSessionData.get(taskId);
        if (handlerDataInCurrentTask == null
                || !handlerDataInCurrentTask.session.equals(
                        CustomTabsSessionToken.getSessionTokenFromIntent(intent))) {
            return null;
        }
        return handlerDataInCurrentTask.activityClass;
    }

    /**
     * Attempts to handle an Intent.
     *
     * Checks whether an incoming intent can be handled by the current {@link SessionHandler}, and
     * if so, delegates the handling to it.
     *
     * @return Whether the active {@link SessionHandler} has handled the intent.
     */
    public boolean handleIntent(Intent intent) {
        SessionHandler handler = getActiveHandlerForIntent(intent);
        return handler != null && handler.handleIntent(intent);
    }

    /** Returns whether the given session is the currently active session. */
    public boolean isActiveSession(@Nullable CustomTabsSessionToken session) {
        return getActiveHandler(session) != null;
    }

    /**
     * Returns the active session handler if it is associated with given session, null otherwise.
     */
    public @Nullable SessionHandler getActiveHandler(@Nullable CustomTabsSessionToken session) {
        if (mActiveSessionHandler == null) return null;
        CustomTabsSessionToken activeSession = mActiveSessionHandler.getSession();
        if (activeSession == null || !activeSession.equals(session)) return null;
        return mActiveSessionHandler;
    }

    private @Nullable SessionHandler getActiveHandlerForIntent(Intent intent) {
        return getActiveHandler(CustomTabsSessionToken.getSessionTokenFromIntent(intent));
    }

    /**
     * Checks whether the given referrer can be used as valid within the Activity launched with the
     * given {@link CustomTabsSessionToken}. For this to be true the token should correspond to the
     * currently in focus custom tab and also the related client should have a verified relationship
     * with the referrer origin. This can only be true for https:// origins.
     *
     * @param token The session token specified in the activity launch intent.
     * @param referrer The referrer url that is to be used.
     * @return Whether the given referrer is a valid first party url to the client that launched
     *         the activity.
     */
    public boolean canActiveHandlerUseReferrer(
            @Nullable CustomTabsSessionToken token, Uri referrer) {
        SessionHandler handler = getActiveHandler(token);
        return handler != null && handler.canUseReferrer(referrer);
    }

    private void ensureSessionCleanUpOnDisconnects() {
        if (mSessionDisconnectCallback != null) return;
        mSessionDisconnectCallback =
                (session) -> {
                    if (session == null) {
                        return;
                    }
                    for (int i = 0; i < mTaskIdToSessionData.size(); i++) {
                        if (session.equals(mTaskIdToSessionData.valueAt(i).session)) {
                            mTaskIdToSessionData.removeAt(i);
                        }
                    }
                };
        mConnection.get().setDisconnectCallback(mSessionDisconnectCallback);
    }
}