content: /*
   * 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.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()) {
              XmlPullParser parser = Xml.newPullParser();
              parser.setInput(fis, StandardCharsets.UTF_8.name());
              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(XmlPullParser 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();
  
              XmlSerializer out = new FastXmlSerializer();
              out.setOutput(fos, StandardCharsets.UTF_8.name());
              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);
      }
  }
  