/*
 * Copyright (C) 2018 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 com.android.server.devicepolicy;

import android.annotation.Nullable;
import android.app.admin.DevicePolicyEventLogger;
import android.app.admin.DevicePolicyManager.InstallSystemUpdateCallback;
import android.app.admin.StartInstallingUpdateCallback;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.Environment;
import android.os.FileUtils;
import android.os.ParcelFileDescriptor;
import android.os.PowerManager;
import android.os.Process;
import android.os.RemoteException;
import android.stats.devicepolicy.DevicePolicyEnums;
import android.util.Log;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

abstract class UpdateInstaller {
    private StartInstallingUpdateCallback mCallback;
    private ParcelFileDescriptor mUpdateFileDescriptor;
    private DevicePolicyConstants mConstants;
    protected Context mContext;

    @Nullable protected File mCopiedUpdateFile;

    static final String TAG = "UpdateInstaller";
    private DevicePolicyManagerService.Injector mInjector;

    protected UpdateInstaller(Context context, ParcelFileDescriptor updateFileDescriptor,
            StartInstallingUpdateCallback callback, DevicePolicyManagerService.Injector injector,
            DevicePolicyConstants constants) {
        mContext = context;
        mCallback = callback;
        mUpdateFileDescriptor = updateFileDescriptor;
        mInjector = injector;
        mConstants = constants;
    }

    public abstract void installUpdateInThread();

    public void startInstallUpdate() {
        mCopiedUpdateFile = null;
        if (!isBatteryLevelSufficient()) {
            notifyCallbackOnError(
                    InstallSystemUpdateCallback.UPDATE_ERROR_BATTERY_LOW,
                    "The battery level must be above "
                            + mConstants.BATTERY_THRESHOLD_NOT_CHARGING + " while not charging or"
                            + "above " + mConstants.BATTERY_THRESHOLD_CHARGING + " while charging");
            return;
        }
        Thread thread = new Thread(() -> {
            mCopiedUpdateFile = copyUpdateFileToDataOtaPackageDir();
            if (mCopiedUpdateFile == null) {
                notifyCallbackOnError(
                        InstallSystemUpdateCallback.UPDATE_ERROR_UNKNOWN,
                        "Error while copying file.");
                return;
            }
            installUpdateInThread();
        });
        thread.setPriority(Process.THREAD_PRIORITY_BACKGROUND);
        thread.start();
    }

    private boolean isBatteryLevelSufficient() {
        Intent batteryStatus = mContext.registerReceiver(
                /* receiver= */ null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
        float batteryPercentage = calculateBatteryPercentage(batteryStatus);
        boolean isBatteryPluggedIn =
                batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, /* defaultValue= */ -1) > 0;
        return isBatteryPluggedIn
                ? batteryPercentage >= mConstants.BATTERY_THRESHOLD_CHARGING
                : batteryPercentage >= mConstants.BATTERY_THRESHOLD_NOT_CHARGING;
    }

    private float calculateBatteryPercentage(Intent batteryStatus) {
        int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, /* defaultValue= */ -1);
        int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, /* defaultValue= */ -1);
        return 100 * level / (float) scale;
    }

    private File copyUpdateFileToDataOtaPackageDir() {
        try {
            File destination = createNewFileWithPermissions();
            copyToFile(destination);
            return destination;
        } catch (IOException e) {
            Log.w(TAG, "Failed to copy update file to OTA directory", e);
            notifyCallbackOnError(
                    InstallSystemUpdateCallback.UPDATE_ERROR_UNKNOWN,
                    Log.getStackTraceString(e));
            return null;
        }
    }

    private File createNewFileWithPermissions() throws IOException {
        File destination = File.createTempFile(
                "update", ".zip", new File(Environment.getDataDirectory() + "/ota_package"));
        FileUtils.setPermissions(
                /* path= */ destination,
                /* mode= */ FileUtils.S_IRWXU | FileUtils.S_IRGRP | FileUtils.S_IROTH,
                /* uid= */ -1, /* gid= */ -1);
        return destination;
    }

    private void copyToFile(File destination) throws IOException {
        try (OutputStream out = new FileOutputStream(destination);
             InputStream in = new ParcelFileDescriptor.AutoCloseInputStream(
                     mUpdateFileDescriptor)) {
            FileUtils.copy(in, out);
        }
    }

    void cleanupUpdateFile() {
        if (mCopiedUpdateFile != null && mCopiedUpdateFile.exists()) {
            mCopiedUpdateFile.delete();
        }
    }

    protected void notifyCallbackOnError(int errorCode, String errorMessage) {
        cleanupUpdateFile();
        DevicePolicyEventLogger
                .createEvent(DevicePolicyEnums.INSTALL_SYSTEM_UPDATE_ERROR)
                .setInt(errorCode)
                .write();
        try {
            mCallback.onStartInstallingUpdateError(errorCode, errorMessage);
        } catch (RemoteException e) {
            Log.d(TAG, "Error while calling callback", e);
        }
    }

    protected void notifyCallbackOnSuccess() {
        cleanupUpdateFile();
        mInjector.powerManagerReboot(PowerManager.REBOOT_REQUESTED_BY_DEVICE_OWNER);
    }
}
