/*
 * Copyright (C) 2022 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 android.server.wm;

import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.server.wm.WindowManagerState.STATE_RESUMED;
import static android.server.wm.jetpack.second.Components.SECOND_UNTRUSTED_EMBEDDING_ACTIVITY;
import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.assumeActivityEmbeddingSupportedDevice;
import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE;
import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_OPEN;

import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.graphics.Rect;
import android.os.Binder;
import android.os.IBinder;
import android.platform.test.annotations.Presubmit;
import android.server.wm.WindowManagerState.Task;
import android.window.TaskFragmentInfo;
import android.window.WindowContainerTransaction;

import androidx.annotation.NonNull;

import org.junit.Before;
import org.junit.Test;

/**
 * Tests that verifies the behaviors of embedding activities in different trusted modes.
 *
 * Build/Install/Run:
 *     atest CtsWindowManagerDeviceTestCases:TaskFragmentTrustedModeTest
 */
@Presubmit
@android.server.wm.annotation.Group2
public class TaskFragmentTrustedModeTest extends TaskFragmentOrganizerTestBase {

    private final ComponentName mTranslucentActivity = new ComponentName(mContext,
            TranslucentActivity.class);

    @Before
    @Override
    public void setUp() throws Exception {
        super.setUp();
        assumeActivityEmbeddingSupportedDevice();
    }

    /**
     * Verifies the visibility of a task fragment that has overlays on top of activities embedded
     * in untrusted mode when there is an overlay over the task fragment.
     */
    @Test
    public void testUntrustedModeTaskFragmentVisibility_overlayTaskFragment() {
        // Create a task fragment with activity in untrusted mode.
        final Rect baseActivityBounds =
                mOwnerActivity.getResources().getConfiguration().windowConfiguration.getBounds();
        final TaskFragmentInfo tf = createTaskFragment(SECOND_UNTRUSTED_EMBEDDING_ACTIVITY,
                partialOverlayRelativeBounds(baseActivityBounds));

        // Start a translucent activity over the TaskFragment.
        createTaskFragment(mTranslucentActivity, partialOverlayRelativeBounds(
                tf.getConfiguration().windowConfiguration.getBounds()));
        waitAndAssertResumedActivity(mTranslucentActivity, "Translucent activity must be resumed.");

        // The task fragment must be made invisible when there is an overlay activity in it.
        final String overlayMessage = "Activities embedded in untrusted mode should be made "
                + "invisible in a task fragment with overlay";
        waitAndAssertStoppedActivity(SECOND_UNTRUSTED_EMBEDDING_ACTIVITY, overlayMessage);
        assertFalse(overlayMessage, mWmState.getTaskFragmentByActivity(
                SECOND_UNTRUSTED_EMBEDDING_ACTIVITY).isVisible());

        // The activity that appeared on top would stay resumed
        assertTrue(overlayMessage, mWmState.hasActivityState(mTranslucentActivity, STATE_RESUMED));
        assertTrue(overlayMessage, mWmState.isActivityVisible(mTranslucentActivity));
        assertTrue(overlayMessage, mWmState.getTaskFragmentByActivity(
                mTranslucentActivity).isVisible());
    }

    /**
     * Verifies the visibility of a task fragment that has overlays on top of activities embedded
     * in untrusted mode when an activity from another process is started on top.
     */
    @Test
    public void testUntrustedModeTaskFragmentVisibility_startActivityInTaskFragment() {
        // Create a task fragment with activity in untrusted mode.
        final Rect baseActivityBounds =
                mOwnerActivity.getResources().getConfiguration().windowConfiguration.getBounds();
        final TaskFragmentInfo taskFragmentInfo = createTaskFragment(
                SECOND_UNTRUSTED_EMBEDDING_ACTIVITY,
                partialOverlayRelativeBounds(baseActivityBounds));

        // Start an activity with a different UID in the TaskFragment.
        final WindowContainerTransaction wct = new WindowContainerTransaction()
                .startActivityInTaskFragment(taskFragmentInfo.getFragmentToken(), mOwnerToken,
                        new Intent().setComponent(mTranslucentActivity),
                        null /* activityOptions */);
        mTaskFragmentOrganizer.applyTransaction(wct, TASK_FRAGMENT_TRANSIT_OPEN,
                false /* shouldApplyIndependently */);
        waitAndAssertResumedActivity(mTranslucentActivity, "Translucent activity must be resumed.");

        // Some activities in the task fragment must be made invisible when there is an overlay.
        final String overlayMessage = "Activities embedded in untrusted mode should be made "
                + "invisible in a task fragment with overlay";
        waitAndAssertStoppedActivity(SECOND_UNTRUSTED_EMBEDDING_ACTIVITY, overlayMessage);

        // The activity that appeared on top would stay resumed, and the task fragment is still
        // visible.
        assertTrue(overlayMessage, mWmState.hasActivityState(mTranslucentActivity, STATE_RESUMED));
        assertTrue(overlayMessage, mWmState.isActivityVisible(mTranslucentActivity));
        assertTrue(overlayMessage, mWmState.getTaskFragmentByActivity(
                SECOND_UNTRUSTED_EMBEDDING_ACTIVITY).isVisible());
    }

    /**
     * Verifies the visibility of a task fragment that has overlays on top of activities embedded
     * in untrusted mode when an activity from another process is reparented on top.
     */
    @Test
    public void testUntrustedModeTaskFragmentVisibility_reparentActivityInTaskFragment() {
        final Activity translucentActivity = startActivity(TranslucentActivity.class);

        // Create a task fragment with activity in untrusted mode.
        final TaskFragmentInfo taskFragmentInfo = createTaskFragment(
                SECOND_UNTRUSTED_EMBEDDING_ACTIVITY);

        // Reparent a translucent activity with a different UID to the TaskFragment.
        final IBinder embeddedActivityToken = getActivityToken(translucentActivity);
        final WindowContainerTransaction wct = new WindowContainerTransaction()
                .reparentActivityToTaskFragment(taskFragmentInfo.getFragmentToken(),
                        embeddedActivityToken);
        mTaskFragmentOrganizer.applyTransaction(wct, TASK_FRAGMENT_TRANSIT_CHANGE,
                false /* shouldApplyIndependently */);
        waitAndAssertResumedActivity(mTranslucentActivity, "Translucent activity must be resumed.");

        // Some activities in the task fragment must be made invisible when there is an overlay.
        final String overlayMessage = "Activities embedded in untrusted mode should be made "
                + "invisible in a task fragment with overlay";
        waitAndAssertStoppedActivity(SECOND_UNTRUSTED_EMBEDDING_ACTIVITY, overlayMessage);

        // The activity that appeared on top would stay resumed, and the task fragment is still
        // visible
        assertTrue(overlayMessage, mWmState.hasActivityState(mTranslucentActivity, STATE_RESUMED));
        assertTrue(overlayMessage, mWmState.isActivityVisible(mTranslucentActivity));
        assertTrue(overlayMessage, mWmState.getTaskFragmentByActivity(
                SECOND_UNTRUSTED_EMBEDDING_ACTIVITY).isVisible());

        // Finishing the overlay activity must make TaskFragment visible again.
        translucentActivity.finish();
        waitAndAssertResumedActivity(SECOND_UNTRUSTED_EMBEDDING_ACTIVITY,
                "Activity must be resumed without overlays");
        assertTrue("Activity must be visible without overlays",
                mWmState.isActivityVisible(SECOND_UNTRUSTED_EMBEDDING_ACTIVITY));
    }

    /**
     * Verifies that when the TaskFragment has embedded activities in untrusted mode, set relative
     * bounds outside of its parent bounds will still set the TaskFragment bounds within its parent.
     */
    @Test
    public void testUntrustedModeTaskFragment_setRelativeBoundsOutsideOfParentBounds() {
        final Task parentTask = mWmState.getTaskByActivity(mOwnerActivityName);
        final Rect parentBounds = new Rect(parentTask.getBounds());
        // Create a TaskFragment with activity embedded in untrusted mode.
        final TaskFragmentInfo info = createTaskFragment(SECOND_UNTRUSTED_EMBEDDING_ACTIVITY);

        // Try to set relative bounds that is larger than its parent bounds.
        mTaskFragmentOrganizer.resetLatch();
        final Rect taskFragRelativeBounds = new Rect(parentBounds);
        taskFragRelativeBounds.offsetTo(0, 0);
        taskFragRelativeBounds.right++;
        final WindowContainerTransaction wct = new WindowContainerTransaction()
                .setRelativeBounds(info.getToken(), taskFragRelativeBounds)
                .setWindowingMode(info.getToken(), WINDOWING_MODE_MULTI_WINDOW);

        // It is allowed to set TaskFragment bounds to outside of its parent bounds.
        mTaskFragmentOrganizer.applyTransaction(wct, TASK_FRAGMENT_TRANSIT_CHANGE,
                false /* shouldApplyIndependently */);

        // Update the windowing mode to make sure the WindowContainerTransaction has been applied.
        mWmState.waitForWithAmState(amState -> {
            final WindowManagerState.TaskFragment tf = amState.getTaskFragmentByActivity(
                    SECOND_UNTRUSTED_EMBEDDING_ACTIVITY);
            return tf != null && tf.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW;
        }, "TaskFragment should have WINDOWING_MODE_MULTI_WINDOW");

        // The TaskFragment bounds should remain in its parent bounds.
        final WindowManagerState.TaskFragment tf = mWmState.getTaskFragmentByActivity(
                SECOND_UNTRUSTED_EMBEDDING_ACTIVITY);
        assertNotNull(tf);
        assertEquals(WINDOWING_MODE_MULTI_WINDOW, tf.getWindowingMode());
        assertEquals(parentBounds, tf.mFullConfiguration.windowConfiguration.getBounds());
    }

    /**
     * Verifies that when the TaskFragment bounds is outside of its parent bounds, it is disallowed
     * to start activity in untrusted mode.
     */
    @Test
    public void testUntrustedModeTaskFragment_startActivityInTaskFragmentOutsideOfParentBounds() {
        final Task parentTask = mWmState.getTaskByActivity(mOwnerActivityName);
        final Rect parentBounds = new Rect(parentTask.getBounds());
        final IBinder errorCallbackToken = new Binder();
        final WindowContainerTransaction wct = new WindowContainerTransaction()
                .setErrorCallbackToken(errorCallbackToken);

        // We check if the TaskFragment bounds is in its parent bounds before launching activity in
        // untrusted mode.
        final Rect taskFragRelativeBounds = new Rect(parentBounds);
        taskFragRelativeBounds.offsetTo(0, 0);
        taskFragRelativeBounds.right++;
        createTaskFragment(SECOND_UNTRUSTED_EMBEDDING_ACTIVITY, taskFragRelativeBounds, wct);

        // It is disallowed to start activity to TaskFragment with bounds outside of its parent
        // in untrusted mode.
        assertTaskFragmentError(errorCallbackToken, SecurityException.class);
    }

    /**
     * Creates relative bounds for a container that would appear on top and partially occlude the
     * provided one.
     */
    @NonNull
    private Rect partialOverlayRelativeBounds(@NonNull Rect baseBounds) {
        final Rect result = new Rect(baseBounds);
        result.offsetTo(0, 0);
        result.inset(50 /* left */, 50 /* top */, 50 /* right */, 50 /* bottom */);
        return result;
    }

    /** Asserts that the organizer received an error callback. */
    private void assertTaskFragmentError(@NonNull IBinder errorCallbackToken,
            @NonNull Class<? extends Throwable> exceptionClass) {
        mTaskFragmentOrganizer.waitForTaskFragmentError();
        assertThat(mTaskFragmentOrganizer.getThrowable()).isInstanceOf(exceptionClass);
        assertThat(mTaskFragmentOrganizer.getErrorCallbackToken()).isEqualTo(errorCallbackToken);
    }

    public static class TranslucentActivity extends FocusableActivity {}
}
