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

import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SystemService;
import android.content.Context;
import android.gsi.AvbPublicKey;
import android.gsi.GsiProgress;
import android.gsi.IGsiService;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.Pair;

/**
 * The DynamicSystemManager offers a mechanism to use a new system image temporarily. After the
 * installation, the device can reboot into this image with a new created /data. This image will
 * last until the next reboot and then the device will go back to the original image. However the
 * installed image and the new created /data are not deleted but disabled. Thus the application can
 * either re-enable the installed image by calling {@link #toggle} or use the {@link #remove} to
 * delete it completely. In other words, there are three device states: no installation, installed
 * and running. The procedure to install a DynamicSystem starts with a {@link #startInstallation},
 * followed by a series of {@link #write} and ends with a {@link commit}. Once the installation is
 * complete, the device state changes from no installation to the installed state and a followed
 * reboot will change its state to running. Note one instance of DynamicSystem can exist on a given
 * device thus the {@link #startInstallation} will fail if the device is currently running a
 * DynamicSystem.
 *
 * @hide
 */
@SystemService(Context.DYNAMIC_SYSTEM_SERVICE)
public class DynamicSystemManager {
    private static final String TAG = "DynamicSystemManager";

    private final IDynamicSystemService mService;

    /** {@hide} */
    public DynamicSystemManager(IDynamicSystemService service) {
        mService = service;
    }

    /** The DynamicSystemManager.Session represents a started session for the installation. */
    public class Session {
        private Session() {}

        /**
         * Set the file descriptor that points to a ashmem which will be used
         * to fetch data during the submitFromAshmem.
         *
         * @param ashmem fd that points to a ashmem
         * @param size size of the ashmem file
         */
        @RequiresPermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
        public boolean setAshmem(ParcelFileDescriptor ashmem, long size) {
            try {
                return mService.setAshmem(ashmem, size);
            } catch (RemoteException e) {
                throw new RuntimeException(e.toString());
            }
        }

        /**
         * Submit bytes to the DSU partition from the ashmem previously set with
         * setAshmem.
         *
         * @param size Number of bytes
         * @return true on success, false otherwise.
         */
        @RequiresPermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
        public boolean submitFromAshmem(int size) {
            try {
                return mService.submitFromAshmem(size);
            } catch (RemoteException e) {
                throw new RuntimeException(e.toString());
            }
        }

        /**
         * Retrieve AVB public key from installing partition.
         *
         * @param dst           Output the AVB public key.
         * @return              true on success, false if partition doesn't have a
         *                      valid VBMeta block to retrieve the AVB key from.
         */
        @RequiresPermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
        public boolean getAvbPublicKey(AvbPublicKey dst) {
            try {
                return mService.getAvbPublicKey(dst);
            } catch (RemoteException e) {
                throw new RuntimeException(e.toString());
            }
        }

        /**
         * Finish write and make device to boot into the it after reboot.
         *
         * @return {@code true} if the call succeeds. {@code false} if there is any native runtime
         *     error.
         */
        @RequiresPermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
        public boolean commit() {
            try {
                return mService.setEnable(true, true);
            } catch (RemoteException e) {
                throw new RuntimeException(e.toString());
            }
        }
    }
    /**
     * Start DynamicSystem installation.
     *
     * @return true if the call succeeds
     */
    @RequiresPermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
    public boolean startInstallation(String dsuSlot) {
        try {
            return mService.startInstallation(dsuSlot);
        } catch (RemoteException e) {
            throw new RuntimeException(e.toString());
        }
    }
    /**
     * Start DynamicSystem installation. This call may take an unbounded amount of time. The caller
     * may use another thread to call the getStartProgress() to get the progress.
     *
     * @param name The DSU partition name
     * @param size Size of the DSU image in bytes
     * @param readOnly True if the partition is read only, e.g. system.
     * @return {@code Integer} an IGsiService.INSTALL_* status code. {@link Session} an installation
     *     session object if successful, otherwise {@code null}.
     */
    @RequiresPermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
    public @NonNull Pair<Integer, Session> createPartition(
            String name, long size, boolean readOnly) {
        try {
            int status = mService.createPartition(name, size, readOnly);
            if (status == IGsiService.INSTALL_OK) {
                return new Pair<>(status, new Session());
            } else {
                return new Pair<>(status, null);
            }
        } catch (RemoteException e) {
            throw new RuntimeException(e.toString());
        }
    }
    /**
     * Complete the current partition installation.
     *
     * @return true if the partition installation completes without error.
     */
    @RequiresPermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
    public boolean closePartition() {
        try {
            return mService.closePartition();
        } catch (RemoteException e) {
            throw new RuntimeException(e.toString());
        }
    }
    /**
     * Finish a previously started installation. Installations without a cooresponding
     * finishInstallation() will be cleaned up during device boot.
     */
    @RequiresPermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
    public boolean finishInstallation() {
        try {
            return mService.finishInstallation();
        } catch (RemoteException e) {
            throw new RuntimeException(e.toString());
        }
    }
    /**
     * Query the progress of the current installation operation. This can be called while the
     * installation is in progress.
     *
     * @return GsiProgress GsiProgress { int status; long bytes_processed; long total_bytes; } The
     *     status field can be IGsiService.STATUS_NO_OPERATION, IGsiService.STATUS_WORKING or
     *     IGsiService.STATUS_COMPLETE.
     */
    @RequiresPermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
    public GsiProgress getInstallationProgress() {
        try {
            return mService.getInstallationProgress();
        } catch (RemoteException e) {
            throw new RuntimeException(e.toString());
        }
    }

    /**
     * Abort the installation process. Note this method must be called in a thread other than the
     * one calling the startInstallation method as the startInstallation method will not return
     * until it is finished.
     *
     * @return {@code true} if the call succeeds. {@code false} if there is no installation
     *     currently.
     */
    @RequiresPermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
    public boolean abort() {
        try {
            return mService.abort();
        } catch (RemoteException e) {
            throw new RuntimeException(e.toString());
        }
    }

    /** @return {@code true} if the device is running a dynamic system */
    @RequiresPermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
    public boolean isInUse() {
        try {
            return mService.isInUse();
        } catch (RemoteException e) {
            throw new RuntimeException(e.toString());
        }
    }

    /** @return {@code true} if the device has a dynamic system installed */
    @RequiresPermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
    public boolean isInstalled() {
        try {
            return mService.isInstalled();
        } catch (RemoteException e) {
            throw new RuntimeException(e.toString());
        }
    }

    /** @return {@code true} if the device has a dynamic system enabled */
    @RequiresPermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
    public boolean isEnabled() {
        try {
            return mService.isEnabled();
        } catch (RemoteException e) {
            throw new RuntimeException(e.toString());
        }
    }

    /**
     * Remove DynamicSystem installation if present
     *
     * @return {@code true} if the call succeeds. {@code false} if there is no installed image.
     */
    @RequiresPermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
    public boolean remove() {
        try {
            return mService.remove();
        } catch (RemoteException e) {
            throw new RuntimeException(e.toString());
        }
    }

    /**
     * Enable or disable DynamicSystem.
     * @return {@code true} if the call succeeds. {@code false} if there is no installed image.
     */
    @RequiresPermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
    public boolean setEnable(boolean enable, boolean oneShot) {
        try {
            return mService.setEnable(enable, oneShot);
        } catch (RemoteException e) {
            throw new RuntimeException(e.toString());
        }
    }

    /**
     * Returns the suggested scratch partition size for overlayFS.
     */
    @RequiresPermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
    public long suggestScratchSize() {
        try {
            return mService.suggestScratchSize();
        } catch (RemoteException e) {
            throw new RuntimeException(e.toString());
        }
    }
}
