/*
 * 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.app.sdksandbox;

import static android.app.sdksandbox.SdkSandboxSystemServiceRegistry.ServiceMutator;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.ApplicationInfo;
import android.content.res.AssetManager;
import android.content.res.Resources;

import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;

import java.io.File;

/**
 * Refers to the context of the SDK loaded in the SDK sandbox process.
 *
 * <p>It is a wrapper of the client application (which loading SDK to the sandbox) context, to
 * represent the context of the SDK loaded by that application.
 *
 * <p>An instance of the {@link SandboxedSdkContext} will be created by the SDK sandbox, and then
 * attached to the {@link SandboxedSdkProvider} after the SDK is loaded.
 *
 * <p>Each sdk will get their own private storage directories and the file storage API on this
 * object will utilize those areas.
 *
 * @hide
 */
public final class SandboxedSdkContext extends ContextWrapper {

    private final Resources mResources;
    private final AssetManager mAssets;
    private final String mClientPackageName;
    private final String mSdkName;
    private final ApplicationInfo mSdkProviderInfo;
    @Nullable private final File mCeDataDir;
    @Nullable private final File mDeDataDir;
    private final SdkSandboxSystemServiceRegistry mSdkSandboxSystemServiceRegistry;
    private final ClassLoader mClassLoader;

    /**
     * Starting from Android U, we customize the base context directly instead of adding information
     * to the ContextWrapper.
     */
    private final boolean mIsCustomizedSdkContextEnabled = SdkLevel.isAtLeastU();

    public SandboxedSdkContext(
            @NonNull Context baseContext,
            @NonNull ClassLoader classLoader,
            @NonNull String clientPackageName,
            @NonNull ApplicationInfo info,
            @NonNull String sdkName,
            @Nullable String sdkCeDataDir,
            @Nullable String sdkDeDataDir) {
        this(
                baseContext,
                classLoader,
                clientPackageName,
                info,
                sdkName,
                sdkCeDataDir,
                sdkDeDataDir,
                SdkSandboxSystemServiceRegistry.getInstance());
    }

    @VisibleForTesting
    public SandboxedSdkContext(
            @NonNull Context baseContext,
            @NonNull ClassLoader classLoader,
            @NonNull String clientPackageName,
            @NonNull ApplicationInfo info,
            @NonNull String sdkName,
            @Nullable String sdkCeDataDir,
            @Nullable String sdkDeDataDir,
            SdkSandboxSystemServiceRegistry sdkSandboxSystemServiceRegistry) {
        super(baseContext);
        mClientPackageName = clientPackageName;
        mSdkName = sdkName;
        mSdkProviderInfo = info;
        Resources resources = null;
        try {
            resources = baseContext.getPackageManager().getResourcesForApplication(info);
        } catch (Exception ignored) {
        }

        if (resources != null) {
            mResources = resources;
            mAssets = resources.getAssets();
        } else {
            mResources = null;
            mAssets = null;
        }

        mCeDataDir = (sdkCeDataDir != null) ? new File(sdkCeDataDir) : null;
        mDeDataDir = (sdkDeDataDir != null) ? new File(sdkDeDataDir) : null;

        mSdkSandboxSystemServiceRegistry = sdkSandboxSystemServiceRegistry;
        mClassLoader = classLoader;
    }

    /**
     * Return a new Context object for the current SandboxedSdkContext but whose storage APIs are
     * backed by sdk specific credential-protected storage.
     *
     * @see Context#isCredentialProtectedStorage()
     */
    @Override
    @NonNull
    public Context createCredentialProtectedStorageContext() {
        Context newBaseContext = getBaseContext().createCredentialProtectedStorageContext();
        return createContextWithNewBase(newBaseContext);
    }

    /**
     * Return a new Context object for the current SandboxedSdkContext but whose storage
     * APIs are backed by sdk specific device-protected storage.
     *
     * @see Context#isDeviceProtectedStorage()
     */
    @Override
    @NonNull
    public Context createDeviceProtectedStorageContext() {
        Context newBaseContext = getBaseContext().createDeviceProtectedStorageContext();
        return createContextWithNewBase(newBaseContext);
    }

    /**
     * Returns a new SandboxedSdkContext object with the same fields of the caller {@link
     * SandboxedSdkContext} but with new base {@link Context}.
     *
     * @param newBaseContext The new base {@link Context} to use.
     */
    @NonNull
    public SandboxedSdkContext createContextWithNewBase(@NonNull Context newBaseContext) {
        return new SandboxedSdkContext(
                newBaseContext,
                mClassLoader,
                mClientPackageName,
                mSdkProviderInfo,
                mSdkName,
                (mCeDataDir != null) ? mCeDataDir.toString() : null,
                (mDeDataDir != null) ? mDeDataDir.toString() : null);
    }

    /**
     * Returns the SDK name defined in the SDK's manifest.
     */
    @NonNull
    public String getSdkName() {
        return mSdkName;
    }

    /**
     * Returns the SDK package name defined in the SDK's manifest.
     *
     * @hide
     */
    @NonNull
    public String getSdkPackageName() {
        return mSdkProviderInfo.packageName;
    }

    /**
     * Returns the package name of the client application corresponding to the sandbox.
     *
     */
    @NonNull
    public String getClientPackageName() {
        return mClientPackageName;
    }

    /** Returns the resources defined in the SDK's .apk file. */
    @Override
    @Nullable
    public Resources getResources() {
        if (mIsCustomizedSdkContextEnabled) {
            return getBaseContext().getResources();
        }
        return mResources;
    }

    /** Returns the assets defined in the SDK's .apk file. */
    @Override
    @Nullable
    public AssetManager getAssets() {
        if (mIsCustomizedSdkContextEnabled) {
            return getBaseContext().getAssets();
        }
        return mAssets;
    }

    /** Returns sdk-specific internal storage directory. */
    @Override
    @Nullable
    public File getDataDir() {
        if (mIsCustomizedSdkContextEnabled) {
            return getBaseContext().getDataDir();
        }

        File res = null;
        if (isCredentialProtectedStorage()) {
            res = mCeDataDir;
        } else if (isDeviceProtectedStorage()) {
            res = mDeDataDir;
        }
        if (res == null) {
            throw new RuntimeException("No data directory found for sdk: " + getSdkName());
        }
        return res;
    }

    @Override
    @Nullable
    public Object getSystemService(String name) {
        if (name == null) {
            return null;
        }
        Object service = getBaseContext().getSystemService(name);
        ServiceMutator serviceMutator = mSdkSandboxSystemServiceRegistry.getServiceMutator(name);
        if (serviceMutator != null) {
            service = serviceMutator.setContext(service, this);
        }
        return service;
    }

    @Override
    public ClassLoader getClassLoader() {
        if (mIsCustomizedSdkContextEnabled) {
            return getBaseContext().getClassLoader();
        }
        return mClassLoader;
    }
}
