/*
 * Copyright (C) 2019 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.os.incremental;

/**
 * Set up files and directories used in an installation session. Currently only used by Incremental
 * Installation. For Incremental installation, the expected outcome of this function is: 0) All the
 * files are in defaultStorage 1) All APK files are in the same directory, bound to mApkStorage, and
 * bound to the InstallerSession's stage dir. The files are linked from mApkStorage to
 * defaultStorage. 2) All lib files are in the sub directories as their names suggest, and in the
 * same parent directory as the APK files. The files are linked from mApkStorage to defaultStorage.
 * 3) OBB files are in another directory that is different from APK files and lib files, bound to
 * mObbStorage. The files are linked from mObbStorage to defaultStorage.
 *
 * @throws IllegalStateException the session is not an Incremental installation session.
 */

import static android.content.pm.PackageInstaller.LOCATION_DATA_APP;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.DataLoaderParams;
import android.content.pm.IDataLoaderStatusListener;
import android.content.pm.IPackageLoadingProgressCallback;
import android.content.pm.InstallationFileParcel;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.UUID;

/**
 * This class manages storage instances used during a package installation session.
 * @hide
 */
public final class IncrementalFileStorages {
    private static final String TAG = "IncrementalFileStorages";

    private static final String SYSTEM_DATA_LOADER_PACKAGE = "android";

    private @NonNull final IncrementalManager mIncrementalManager;
    private @NonNull final File mStageDir;
    private @Nullable IncrementalStorage mInheritedStorage;
    private @Nullable IncrementalStorage mDefaultStorage;

    /**
     * Set up files and directories used in an installation session. Only used by Incremental.
     * All the files will be created in defaultStorage.
     *
     * @throws IllegalStateException the session is not an Incremental installation session.
     * @throws IOException if fails to setup files or directories.
     */
    public static IncrementalFileStorages initialize(Context context,
            @NonNull File stageDir,
            @Nullable File inheritedDir,
            @NonNull DataLoaderParams dataLoaderParams,
            @Nullable IDataLoaderStatusListener statusListener,
            @Nullable StorageHealthCheckParams healthCheckParams,
            @Nullable IStorageHealthListener healthListener,
            @NonNull List<InstallationFileParcel> addedFiles,
            @NonNull PerUidReadTimeouts[] perUidReadTimeouts,
            @Nullable IPackageLoadingProgressCallback progressCallback) throws IOException {
        IncrementalManager incrementalManager = (IncrementalManager) context.getSystemService(
                Context.INCREMENTAL_SERVICE);
        if (incrementalManager == null) {
            throw new IOException("Failed to obtain incrementalManager.");
        }

        final IncrementalFileStorages result = new IncrementalFileStorages(stageDir, inheritedDir,
                incrementalManager, dataLoaderParams);
        for (InstallationFileParcel file : addedFiles) {
            if (file.location == LOCATION_DATA_APP) {
                try {
                    result.addApkFile(file);
                } catch (IOException e) {
                    throw new IOException(
                            "Failed to add file to IncFS: " + file.name + ", reason: ", e);
                }
            } else {
                throw new IOException("Unknown file location: " + file.location);
            }
        }
        // Register progress loading callback after files have been added
        if (progressCallback != null) {
            incrementalManager.registerLoadingProgressCallback(stageDir.getAbsolutePath(),
                    progressCallback);
        }
        result.startLoading(dataLoaderParams, statusListener, healthCheckParams, healthListener,
                perUidReadTimeouts);

        return result;
    }

    private IncrementalFileStorages(@NonNull File stageDir,
            @Nullable File inheritedDir,
            @NonNull IncrementalManager incrementalManager,
            @NonNull DataLoaderParams dataLoaderParams) throws IOException {
        try {
            mStageDir = stageDir;
            mIncrementalManager = incrementalManager;
            if (inheritedDir != null && IncrementalManager.isIncrementalPath(
                    inheritedDir.getAbsolutePath())) {
                mInheritedStorage = mIncrementalManager.openStorage(
                        inheritedDir.getAbsolutePath());
                if (mInheritedStorage != null) {
                    boolean systemDataLoader = SYSTEM_DATA_LOADER_PACKAGE.equals(
                            dataLoaderParams.getComponentName().getPackageName());
                    if (systemDataLoader && !mInheritedStorage.isFullyLoaded()) {
                        // System data loader does not support incomplete storages.
                        throw new IOException("Inherited storage has missing pages.");
                    }

                    mDefaultStorage = mIncrementalManager.createStorage(stageDir.getAbsolutePath(),
                            mInheritedStorage, IncrementalManager.CREATE_MODE_CREATE
                                    | IncrementalManager.CREATE_MODE_TEMPORARY_BIND);
                    if (mDefaultStorage == null) {
                        throw new IOException(
                                "Couldn't create linked incremental storage at " + stageDir);
                    }
                    return;
                }
            }

            mDefaultStorage = mIncrementalManager.createStorage(stageDir.getAbsolutePath(),
                    dataLoaderParams, IncrementalManager.CREATE_MODE_CREATE
                            | IncrementalManager.CREATE_MODE_TEMPORARY_BIND);
            if (mDefaultStorage == null) {
                throw new IOException(
                        "Couldn't create incremental storage at " + stageDir);
            }
        } catch (IOException e) {
            cleanUp();
            throw e;
        }
    }

    private void addApkFile(@NonNull InstallationFileParcel apk) throws IOException {
        final String apkName = apk.name;
        final File targetFile = new File(mStageDir, apkName);
        if (!targetFile.exists()) {
            mDefaultStorage.makeFile(apkName, apk.size, 0777, null, apk.metadata,
                    apk.signature, null);
        }
    }

    /**
     * Starts or re-starts loading of data.
     */
    public void startLoading(
            @NonNull DataLoaderParams dataLoaderParams,
            @Nullable IDataLoaderStatusListener statusListener,
            @Nullable StorageHealthCheckParams healthCheckParams,
            @Nullable IStorageHealthListener healthListener,
            @NonNull PerUidReadTimeouts[] perUidReadTimeouts) throws IOException {
        if (!mDefaultStorage.startLoading(dataLoaderParams, statusListener, healthCheckParams,
                healthListener, perUidReadTimeouts)) {
            throw new IOException(
                    "Failed to start or restart loading data for Incremental installation.");
        }
    }

    /**
     * Creates file in default storage and sets its content.
     */
    public void makeFile(@NonNull String name, @NonNull byte[] content,
            @NonNull int mode) throws IOException {
        mDefaultStorage.makeFile(name, content.length, mode, UUID.randomUUID(),
                null, null, content);
    }

    /**
     * Creates a hardlink from inherited storage to default.
     */
    public boolean makeLink(@NonNull String relativePath, @NonNull String fromBase,
            @NonNull String toBase) throws IOException {
        if (mInheritedStorage == null) {
            return false;
        }
        final File sourcePath = new File(fromBase, relativePath);
        final File destPath = new File(toBase, relativePath);
        mInheritedStorage.makeLink(sourcePath.getAbsolutePath(), mDefaultStorage,
                destPath.getAbsolutePath());
        return true;
    }

    /**
     * Permanently disables readlogs.
     */
    public void disallowReadLogs() {
        mDefaultStorage.disallowReadLogs();
    }

    /**
     * Resets the states and unbinds storage instances for an installation session.
     */
    public void cleanUpAndMarkComplete() {
        IncrementalStorage defaultStorage = cleanUp();
        if (defaultStorage != null) {
            defaultStorage.onInstallationComplete();
        }
    }

    private IncrementalStorage cleanUp() {
        IncrementalStorage defaultStorage = mDefaultStorage;
        mInheritedStorage = null;
        mDefaultStorage = null;
        if (defaultStorage == null) {
            return null;
        }

        try {
            mIncrementalManager.unregisterLoadingProgressCallbacks(mStageDir.getAbsolutePath());
            defaultStorage.unBind(mStageDir.getAbsolutePath());
        } catch (IOException ignored) {
        }
        return defaultStorage;
    }
}
