/*
 * Copyright 2017 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.app.servertransaction;

import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER;

import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityClient;
import android.app.ActivityOptions.SceneTransitionInfo;
import android.app.ActivityThread.ActivityClientRecord;
import android.app.ClientTransactionHandler;
import android.app.IActivityClientController;
import android.app.ProfilerInfo;
import android.app.ResultInfo;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
import android.os.BaseBundle;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcel;
import android.os.PersistableBundle;
import android.os.Trace;
import android.window.ActivityWindowInfo;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IVoiceInteractor;
import com.android.internal.content.ReferrerIntent;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * Request to launch an activity.
 * @hide
 */
public class LaunchActivityItem extends ClientTransactionItem {

    private IBinder mActivityToken;
    @UnsupportedAppUsage
    private Intent mIntent;
    private int mIdent;
    @UnsupportedAppUsage
    private ActivityInfo mInfo;
    private Configuration mCurConfig;
    private Configuration mOverrideConfig;
    private int mDeviceId;
    private String mReferrer;
    private IVoiceInteractor mVoiceInteractor;
    private int mProcState;
    private Bundle mState;
    private PersistableBundle mPersistentState;
    private List<ResultInfo> mPendingResults;
    private List<ReferrerIntent> mPendingNewIntents;
    private SceneTransitionInfo mSceneTransitionInfo;
    private boolean mIsForward;
    private ProfilerInfo mProfilerInfo;
    private IBinder mAssistToken;
    private IBinder mShareableActivityToken;
    private boolean mLaunchedFromBubble;
    private IBinder mTaskFragmentToken;
    private IBinder mInitialCallerInfoAccessToken;
    private ActivityWindowInfo mActivityWindowInfo;

    /**
     * It is only non-null if the process is the first time to launch activity. It is only an
     * optimization for quick look up of the interface so the field is ignored for comparison.
     */
    private IActivityClientController mActivityClientController;

    @Override
    public void preExecute(@NonNull ClientTransactionHandler client) {
        client.countLaunchingActivities(1);
        client.updateProcessState(mProcState, false);
        CompatibilityInfo.applyOverrideScaleIfNeeded(mCurConfig);
        CompatibilityInfo.applyOverrideScaleIfNeeded(mOverrideConfig);
        client.updatePendingConfiguration(mCurConfig);
        if (mActivityClientController != null) {
            ActivityClient.setActivityClientController(mActivityClientController);
        }
    }

    @Override
    public void execute(@NonNull ClientTransactionHandler client,
            @NonNull PendingTransactionActions pendingActions) {
        Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
        final ActivityClientRecord r = new ActivityClientRecord(mActivityToken, mIntent, mIdent,
                mInfo, mOverrideConfig, mReferrer, mVoiceInteractor, mState, mPersistentState,
                mPendingResults, mPendingNewIntents, mSceneTransitionInfo, mIsForward,
                mProfilerInfo, client, mAssistToken, mShareableActivityToken, mLaunchedFromBubble,
                mTaskFragmentToken, mInitialCallerInfoAccessToken, mActivityWindowInfo);
        client.handleLaunchActivity(r, pendingActions, mDeviceId, null /* customIntent */);
        Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
    }

    @Override
    public void postExecute(@NonNull ClientTransactionHandler client,
            @NonNull PendingTransactionActions pendingActions) {
        client.countLaunchingActivities(-1);
    }

    // ObjectPoolItem implementation

    private LaunchActivityItem() {}

    /** Obtain an instance initialized with provided params. */
    @NonNull
    public static LaunchActivityItem obtain(@NonNull IBinder activityToken, @NonNull Intent intent,
            int ident, @NonNull ActivityInfo info, @NonNull Configuration curConfig,
            @NonNull Configuration overrideConfig, int deviceId, @Nullable String referrer,
            @Nullable IVoiceInteractor voiceInteractor, int procState, @Nullable Bundle state,
            @Nullable PersistableBundle persistentState, @Nullable List<ResultInfo> pendingResults,
            @Nullable List<ReferrerIntent> pendingNewIntents,
            @Nullable SceneTransitionInfo sceneTransitionInfo,
            boolean isForward, @Nullable ProfilerInfo profilerInfo, @NonNull IBinder assistToken,
            @Nullable IActivityClientController activityClientController,
            @NonNull IBinder shareableActivityToken, boolean launchedFromBubble,
            @Nullable IBinder taskFragmentToken, @NonNull IBinder initialCallerInfoAccessToken,
            @NonNull ActivityWindowInfo activityWindowInfo) {
        LaunchActivityItem instance = ObjectPool.obtain(LaunchActivityItem.class);
        if (instance == null) {
            instance = new LaunchActivityItem();
        }
        setValues(instance, activityToken, new Intent(intent), ident,  new ActivityInfo(info),
                new Configuration(curConfig), new Configuration(overrideConfig), deviceId,
                referrer, voiceInteractor, procState,
                state != null ? new Bundle(state) : null,
                persistentState != null ? new PersistableBundle(persistentState) : null,
                pendingResults != null ? new ArrayList<>(pendingResults) : null,
                pendingNewIntents != null ? new ArrayList<>(pendingNewIntents) : null,
                sceneTransitionInfo, isForward,
                profilerInfo != null ? new ProfilerInfo(profilerInfo) : null,
                assistToken, activityClientController, shareableActivityToken,
                launchedFromBubble, taskFragmentToken, initialCallerInfoAccessToken,
                new ActivityWindowInfo(activityWindowInfo));

        return instance;
    }

    @VisibleForTesting(visibility = PACKAGE)
    @NonNull
    @Override
    public IBinder getActivityToken() {
        return mActivityToken;
    }

    @Override
    public void recycle() {
        setValues(this, null, null, 0, null, null, null, 0, null, null, 0, null, null, null, null,
                null, false, null, null, null, null, false, null, null, null);
        ObjectPool.recycle(this);
    }

    // Parcelable implementation

    /** Write from Parcel. */
    @Override
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        dest.writeStrongBinder(mActivityToken);
        dest.writeTypedObject(mIntent, flags);
        dest.writeInt(mIdent);
        dest.writeTypedObject(mInfo, flags);
        dest.writeTypedObject(mCurConfig, flags);
        dest.writeTypedObject(mOverrideConfig, flags);
        dest.writeInt(mDeviceId);
        dest.writeString(mReferrer);
        dest.writeStrongInterface(mVoiceInteractor);
        dest.writeInt(mProcState);
        dest.writeBundle(mState);
        dest.writePersistableBundle(mPersistentState);
        dest.writeTypedList(mPendingResults, flags);
        dest.writeTypedList(mPendingNewIntents, flags);
        dest.writeTypedObject(mSceneTransitionInfo, flags);
        dest.writeBoolean(mIsForward);
        dest.writeTypedObject(mProfilerInfo, flags);
        dest.writeStrongBinder(mAssistToken);
        dest.writeStrongInterface(mActivityClientController);
        dest.writeStrongBinder(mShareableActivityToken);
        dest.writeBoolean(mLaunchedFromBubble);
        dest.writeStrongBinder(mTaskFragmentToken);
        dest.writeStrongBinder(mInitialCallerInfoAccessToken);
        dest.writeTypedObject(mActivityWindowInfo, flags);
    }

    /** Read from Parcel. */
    private LaunchActivityItem(@NonNull Parcel in) {
        setValues(this, in.readStrongBinder(), in.readTypedObject(Intent.CREATOR), in.readInt(),
                in.readTypedObject(ActivityInfo.CREATOR), in.readTypedObject(Configuration.CREATOR),
                in.readTypedObject(Configuration.CREATOR), in.readInt(), in.readString(),
                IVoiceInteractor.Stub.asInterface(in.readStrongBinder()), in.readInt(),
                in.readBundle(getClass().getClassLoader()),
                in.readPersistableBundle(getClass().getClassLoader()),
                in.createTypedArrayList(ResultInfo.CREATOR),
                in.createTypedArrayList(ReferrerIntent.CREATOR),
                in.readTypedObject(SceneTransitionInfo.CREATOR),
                in.readBoolean(),
                in.readTypedObject(ProfilerInfo.CREATOR),
                in.readStrongBinder(),
                IActivityClientController.Stub.asInterface(in.readStrongBinder()),
                in.readStrongBinder(),
                in.readBoolean(),
                in.readStrongBinder(),
                in.readStrongBinder(),
                in.readTypedObject(ActivityWindowInfo.CREATOR));
    }

    public static final @NonNull Creator<LaunchActivityItem> CREATOR = new Creator<>() {
        public LaunchActivityItem createFromParcel(@NonNull Parcel in) {
            return new LaunchActivityItem(in);
        }

        public LaunchActivityItem[] newArray(int size) {
            return new LaunchActivityItem[size];
        }
    };

    @Override
    public boolean equals(@Nullable Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        final LaunchActivityItem other = (LaunchActivityItem) o;
        final boolean intentsEqual = (mIntent == null && other.mIntent == null)
                || (mIntent != null && mIntent.filterEquals(other.mIntent));
        return intentsEqual
                && Objects.equals(mActivityToken, other.mActivityToken) && mIdent == other.mIdent
                && activityInfoEqual(other.mInfo) && Objects.equals(mCurConfig, other.mCurConfig)
                && Objects.equals(mOverrideConfig, other.mOverrideConfig)
                && mDeviceId == other.mDeviceId
                && Objects.equals(mReferrer, other.mReferrer)
                && mProcState == other.mProcState && areBundlesEqualRoughly(mState, other.mState)
                && areBundlesEqualRoughly(mPersistentState, other.mPersistentState)
                && Objects.equals(mPendingResults, other.mPendingResults)
                && Objects.equals(mPendingNewIntents, other.mPendingNewIntents)
                && (mSceneTransitionInfo == null) == (other.mSceneTransitionInfo == null)
                && mIsForward == other.mIsForward
                && Objects.equals(mProfilerInfo, other.mProfilerInfo)
                && Objects.equals(mAssistToken, other.mAssistToken)
                && Objects.equals(mShareableActivityToken, other.mShareableActivityToken)
                && Objects.equals(mTaskFragmentToken, other.mTaskFragmentToken)
                && Objects.equals(mInitialCallerInfoAccessToken,
                        other.mInitialCallerInfoAccessToken)
                && Objects.equals(mActivityWindowInfo, other.mActivityWindowInfo);
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + Objects.hashCode(mActivityToken);
        result = 31 * result + mIntent.filterHashCode();
        result = 31 * result + mIdent;
        result = 31 * result + Objects.hashCode(mCurConfig);
        result = 31 * result + Objects.hashCode(mOverrideConfig);
        result = 31 * result + mDeviceId;
        result = 31 * result + Objects.hashCode(mReferrer);
        result = 31 * result + Objects.hashCode(mProcState);
        result = 31 * result + getRoughBundleHashCode(mState);
        result = 31 * result + getRoughBundleHashCode(mPersistentState);
        result = 31 * result + Objects.hashCode(mPendingResults);
        result = 31 * result + Objects.hashCode(mPendingNewIntents);
        result = 31 * result + (mSceneTransitionInfo != null ? 1 : 0);
        result = 31 * result + (mIsForward ? 1 : 0);
        result = 31 * result + Objects.hashCode(mProfilerInfo);
        result = 31 * result + Objects.hashCode(mAssistToken);
        result = 31 * result + Objects.hashCode(mShareableActivityToken);
        result = 31 * result + Objects.hashCode(mTaskFragmentToken);
        result = 31 * result + Objects.hashCode(mInitialCallerInfoAccessToken);
        result = 31 * result + Objects.hashCode(mActivityWindowInfo);
        return result;
    }

    private boolean activityInfoEqual(@Nullable ActivityInfo other) {
        if (mInfo == null) {
            return other == null;
        }
        return other != null && mInfo.flags == other.flags
                && mInfo.getMaxAspectRatio() == other.getMaxAspectRatio()
                && Objects.equals(mInfo.launchToken, other.launchToken)
                && Objects.equals(mInfo.getComponentName(), other.getComponentName());
    }

    /**
     * This method may be used to compare a parceled item with another unparceled item, and the
     * parceled bundle may contain customized class that will raise BadParcelableException when
     * unparceling if a customized class loader is not set to the bundle. So the hash code is
     * simply determined by the bundle is empty or not.
     */
    private static int getRoughBundleHashCode(@Nullable BaseBundle bundle) {
        return (bundle == null || bundle.isDefinitelyEmpty()) ? 0 : 1;
    }

    /** Compares the bundles without unparceling them (avoid BadParcelableException). */
    private static boolean areBundlesEqualRoughly(@Nullable BaseBundle a, @Nullable BaseBundle b) {
        return getRoughBundleHashCode(a) == getRoughBundleHashCode(b);
    }

    @Override
    public String toString() {
        return "LaunchActivityItem{activityToken=" + mActivityToken
                + ",intent=" + mIntent
                + ",ident=" + mIdent
                + ",info=" + mInfo
                + ",curConfig=" + mCurConfig
                + ",overrideConfig=" + mOverrideConfig
                + ",deviceId=" + mDeviceId
                + ",referrer=" + mReferrer
                + ",procState=" + mProcState
                + ",state=" + mState
                + ",persistentState=" + mPersistentState
                + ",pendingResults=" + mPendingResults
                + ",pendingNewIntents=" + mPendingNewIntents
                + ",sceneTransitionInfo=" + mSceneTransitionInfo
                + ",profilerInfo=" + mProfilerInfo
                + ",assistToken=" + mAssistToken
                + ",shareableActivityToken=" + mShareableActivityToken
                + ",activityWindowInfo=" + mActivityWindowInfo
                + "}";
    }

    // Using the same method to set and clear values to make sure we don't forget anything
    private static void setValues(@Nullable LaunchActivityItem instance,
            @Nullable IBinder activityToken, @Nullable Intent intent, int ident,
            @Nullable ActivityInfo info, @Nullable Configuration curConfig,
            @Nullable Configuration overrideConfig, int deviceId,
            @Nullable String referrer, @Nullable IVoiceInteractor voiceInteractor,
            int procState, @Nullable Bundle state, @Nullable PersistableBundle persistentState,
            @Nullable List<ResultInfo> pendingResults,
            @Nullable List<ReferrerIntent> pendingNewIntents,
            @Nullable SceneTransitionInfo sceneTransitionInfo, boolean isForward,
            @Nullable ProfilerInfo profilerInfo, @Nullable IBinder assistToken,
            @Nullable IActivityClientController activityClientController,
            @Nullable IBinder shareableActivityToken, boolean launchedFromBubble,
            @Nullable IBinder taskFragmentToken, @Nullable IBinder initialCallerInfoAccessToken,
            @Nullable ActivityWindowInfo activityWindowInfo) {
        instance.mActivityToken = activityToken;
        instance.mIntent = intent;
        instance.mIdent = ident;
        instance.mInfo = info;
        instance.mCurConfig = curConfig;
        instance.mOverrideConfig = overrideConfig;
        instance.mDeviceId = deviceId;
        instance.mReferrer = referrer;
        instance.mVoiceInteractor = voiceInteractor;
        instance.mProcState = procState;
        instance.mState = state;
        instance.mPersistentState = persistentState;
        instance.mPendingResults = pendingResults;
        instance.mPendingNewIntents = pendingNewIntents;
        instance.mSceneTransitionInfo = sceneTransitionInfo;
        instance.mIsForward = isForward;
        instance.mProfilerInfo = profilerInfo;
        instance.mAssistToken = assistToken;
        instance.mActivityClientController = activityClientController;
        instance.mShareableActivityToken = shareableActivityToken;
        instance.mLaunchedFromBubble = launchedFromBubble;
        instance.mTaskFragmentToken = taskFragmentToken;
        instance.mInitialCallerInfoAccessToken = initialCallerInfoAccessToken;
        instance.mActivityWindowInfo = activityWindowInfo;
    }
}
