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

import static android.os.SystemUpdateManager.KEY_STATUS;
import static android.os.SystemUpdateManager.STATUS_IDLE;
import static android.os.SystemUpdateManager.STATUS_UNKNOWN;

import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.END_TAG;
import static org.xmlpull.v1.XmlPullParser.START_TAG;

import android.Manifest;
import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.ISystemUpdateManager;
import android.os.PersistableBundle;
import android.os.SystemUpdateManager;
import android.provider.Settings;
import android.util.AtomicFile;
import android.util.Slog;
import android.util.TypedXmlPullParser;
import android.util.TypedXmlSerializer;
import android.util.Xml;

import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.XmlUtils;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class SystemUpdateManagerService extends ISystemUpdateManager.Stub {

    private static final String TAG = "SystemUpdateManagerService";

    private static final int UID_UNKNOWN = -1;

    private static final String INFO_FILE = "system-update-info.xml";
    private static final int INFO_FILE_VERSION = 0;
    private static final String TAG_INFO = "info";
    private static final String KEY_VERSION = "version";
    private static final String KEY_UID = "uid";
    private static final String KEY_BOOT_COUNT = "boot-count";
    private static final String KEY_INFO_BUNDLE = "info-bundle";

    private final Context mContext;
    private final AtomicFile mFile;
    private final Object mLock = new Object();
    private int mLastUid = UID_UNKNOWN;
    private int mLastStatus = STATUS_UNKNOWN;

    public SystemUpdateManagerService(Context context) {
        mContext = context;
        mFile = new AtomicFile(new File(Environment.getDataSystemDirectory(), INFO_FILE));

        // Populate mLastUid and mLastStatus.
        synchronized (mLock) {
            loadSystemUpdateInfoLocked();
        }
    }

    @Override
    public void updateSystemUpdateInfo(PersistableBundle infoBundle) {
        mContext.enforceCallingOrSelfPermission(Manifest.permission.RECOVERY, TAG);

        int status = infoBundle.getInt(KEY_STATUS, STATUS_UNKNOWN);
        if (status == STATUS_UNKNOWN) {
            Slog.w(TAG, "Invalid status info. Ignored");
            return;
        }

        // There could be multiple updater apps running on a device. But only one at most should
        // be active (i.e. with a pending update), with the rest reporting idle status. We will
        // only accept the reported status if any of the following conditions holds:
        //   a) none has been reported before;
        //   b) the current on-file status was last reported by the same caller;
        //   c) an active update is being reported.
        int uid = Binder.getCallingUid();
        if (mLastUid == UID_UNKNOWN || mLastUid == uid || status != STATUS_IDLE) {
            synchronized (mLock) {
                saveSystemUpdateInfoLocked(infoBundle, uid);
            }
        } else {
            Slog.i(TAG, "Inactive updater reporting IDLE status. Ignored");
        }
    }

    @Override
    public Bundle retrieveSystemUpdateInfo() {
        if (mContext.checkCallingOrSelfPermission(Manifest.permission.READ_SYSTEM_UPDATE_INFO)
                == PackageManager.PERMISSION_DENIED
                && mContext.checkCallingOrSelfPermission(Manifest.permission.RECOVERY)
                == PackageManager.PERMISSION_DENIED) {
            throw new SecurityException("Can't read system update info. Requiring "
                    + "READ_SYSTEM_UPDATE_INFO or RECOVERY permission.");
        }

        synchronized (mLock) {
            return loadSystemUpdateInfoLocked();
        }
    }

    // Reads and validates the info file. Returns the loaded info bundle on success; or a default
    // info bundle with UNKNOWN status.
    private Bundle loadSystemUpdateInfoLocked() {
        PersistableBundle loadedBundle = null;
        try (FileInputStream fis = mFile.openRead()) {
            TypedXmlPullParser parser = Xml.resolvePullParser(fis);
            loadedBundle = readInfoFileLocked(parser);
        } catch (FileNotFoundException e) {
            Slog.i(TAG, "No existing info file " + mFile.getBaseFile());
        } catch (XmlPullParserException e) {
            Slog.e(TAG, "Failed to parse the info file:", e);
        } catch (IOException e) {
            Slog.e(TAG, "Failed to read the info file:", e);
        }

        // Validate the loaded bundle.
        if (loadedBundle == null) {
            return removeInfoFileAndGetDefaultInfoBundleLocked();
        }

        int version = loadedBundle.getInt(KEY_VERSION, -1);
        if (version == -1) {
            Slog.w(TAG, "Invalid info file (invalid version). Ignored");
            return removeInfoFileAndGetDefaultInfoBundleLocked();
        }

        int lastUid = loadedBundle.getInt(KEY_UID, -1);
        if (lastUid == -1) {
            Slog.w(TAG, "Invalid info file (invalid UID). Ignored");
            return removeInfoFileAndGetDefaultInfoBundleLocked();
        }

        int lastBootCount = loadedBundle.getInt(KEY_BOOT_COUNT, -1);
        if (lastBootCount == -1 || lastBootCount != getBootCount()) {
            Slog.w(TAG, "Outdated info file. Ignored");
            return removeInfoFileAndGetDefaultInfoBundleLocked();
        }

        PersistableBundle infoBundle = loadedBundle.getPersistableBundle(KEY_INFO_BUNDLE);
        if (infoBundle == null) {
            Slog.w(TAG, "Invalid info file (missing info). Ignored");
            return removeInfoFileAndGetDefaultInfoBundleLocked();
        }

        int lastStatus = infoBundle.getInt(KEY_STATUS, STATUS_UNKNOWN);
        if (lastStatus == STATUS_UNKNOWN) {
            Slog.w(TAG, "Invalid info file (invalid status). Ignored");
            return removeInfoFileAndGetDefaultInfoBundleLocked();
        }

        // Everything looks good upon reaching this point.
        mLastStatus = lastStatus;
        mLastUid = lastUid;
        return new Bundle(infoBundle);
    }

    private void saveSystemUpdateInfoLocked(PersistableBundle infoBundle, int uid) {
        // Wrap the incoming bundle with extra info (e.g. version, uid, boot count). We use nested
        // PersistableBundle to avoid manually parsing XML attributes when loading the info back.
        PersistableBundle outBundle = new PersistableBundle();
        outBundle.putPersistableBundle(KEY_INFO_BUNDLE, infoBundle);
        outBundle.putInt(KEY_VERSION, INFO_FILE_VERSION);
        outBundle.putInt(KEY_UID, uid);
        outBundle.putInt(KEY_BOOT_COUNT, getBootCount());

        // Only update the info on success.
        if (writeInfoFileLocked(outBundle)) {
            mLastUid = uid;
            mLastStatus = infoBundle.getInt(KEY_STATUS);
        }
    }

    // Performs I/O work only, without validating the loaded info.
    @Nullable
    private PersistableBundle readInfoFileLocked(TypedXmlPullParser parser)
            throws XmlPullParserException, IOException {
        int type;
        while ((type = parser.next()) != END_DOCUMENT) {
            if (type == START_TAG && TAG_INFO.equals(parser.getName())) {
                return PersistableBundle.restoreFromXml(parser);
            }
        }
        return null;
    }

    private boolean writeInfoFileLocked(PersistableBundle outBundle) {
        FileOutputStream fos = null;
        try {
            fos = mFile.startWrite();

            TypedXmlSerializer out = Xml.resolveSerializer(fos);
            out.startDocument(null, true);

            out.startTag(null, TAG_INFO);
            outBundle.saveToXml(out);
            out.endTag(null, TAG_INFO);

            out.endDocument();
            mFile.finishWrite(fos);
            return true;
        } catch (IOException | XmlPullParserException e) {
            Slog.e(TAG, "Failed to save the info file:", e);
            if (fos != null) {
                mFile.failWrite(fos);
            }
        }
        return false;
    }

    private Bundle removeInfoFileAndGetDefaultInfoBundleLocked() {
        if (mFile.exists()) {
            Slog.i(TAG, "Removing info file");
            mFile.delete();
        }

        mLastStatus = STATUS_UNKNOWN;
        mLastUid = UID_UNKNOWN;
        Bundle infoBundle = new Bundle();
        infoBundle.putInt(KEY_STATUS, STATUS_UNKNOWN);
        return infoBundle;
    }

    private int getBootCount() {
        return Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.BOOT_COUNT, 0);
    }
}
