content: /*
   * Copyright (C) 2017 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.timezone;
  
  import static android.app.timezone.RulesState.DISTRO_STATUS_INSTALLED;
  import static android.app.timezone.RulesState.DISTRO_STATUS_NONE;
  import static android.app.timezone.RulesState.DISTRO_STATUS_UNKNOWN;
  import static android.app.timezone.RulesState.STAGED_OPERATION_INSTALL;
  import static android.app.timezone.RulesState.STAGED_OPERATION_NONE;
  import static android.app.timezone.RulesState.STAGED_OPERATION_UNINSTALL;
  import static android.app.timezone.RulesState.STAGED_OPERATION_UNKNOWN;
  
  import android.app.timezone.Callback;
  import android.app.timezone.DistroFormatVersion;
  import android.app.timezone.DistroRulesVersion;
  import android.app.timezone.ICallback;
  import android.app.timezone.IRulesManager;
  import android.app.timezone.RulesManager;
  import android.app.timezone.RulesState;
  import android.content.Context;
  import android.os.ParcelFileDescriptor;
  import android.os.RemoteException;
  import android.util.Slog;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.server.EventLogTags;
  import com.android.server.SystemService;
  import com.android.timezone.distro.DistroException;
  import com.android.timezone.distro.DistroVersion;
  import com.android.timezone.distro.StagedDistroOperation;
  import com.android.timezone.distro.TimeZoneDistro;
  import com.android.timezone.distro.installer.TimeZoneDistroInstaller;
  
  import libcore.icu.ICU;
  import libcore.timezone.TimeZoneDataFiles;
  import libcore.timezone.TimeZoneFinder;
  import libcore.timezone.TzDataSetVersion;
  import libcore.timezone.ZoneInfoDB;
  
  import java.io.File;
  import java.io.FileDescriptor;
  import java.io.FileInputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.PrintWriter;
  import java.util.Arrays;
  import java.util.concurrent.Executor;
  import java.util.concurrent.atomic.AtomicBoolean;
  
  public final class RulesManagerService extends IRulesManager.Stub {
  
      private static final String TAG = "timezone.RulesManagerService";
  
      /** The distro format supported by this device. */
      @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
      static final DistroFormatVersion DISTRO_FORMAT_VERSION_SUPPORTED =
              new DistroFormatVersion(
                      TzDataSetVersion.currentFormatMajorVersion(),
                      TzDataSetVersion.currentFormatMinorVersion());
  
      public static class Lifecycle extends SystemService {
          public Lifecycle(Context context) {
              super(context);
          }
  
          @Override
          public void onStart() {
              RulesManagerService service = RulesManagerService.create(getContext());
              service.start();
  
              // Publish the binder service so it can be accessed from other (appropriately
              // permissioned) processes.
              publishBinderService(Context.TIME_ZONE_RULES_MANAGER_SERVICE, service);
  
              // Publish the service instance locally so we can use it directly from within the system
              // server from TimeZoneUpdateIdler.
              publishLocalService(RulesManagerService.class, service);
          }
      }
  
      @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
      static final String REQUIRED_UPDATER_PERMISSION =
              android.Manifest.permission.UPDATE_TIME_ZONE_RULES;
      @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
      static final String REQUIRED_QUERY_PERMISSION =
              android.Manifest.permission.QUERY_TIME_ZONE_RULES;
  
      private final AtomicBoolean mOperationInProgress = new AtomicBoolean(false);
      private final PermissionHelper mPermissionHelper;
      private final PackageTracker mPackageTracker;
      private final Executor mExecutor;
      private final RulesManagerIntentHelper mIntentHelper;
      private final TimeZoneDistroInstaller mInstaller;
  
      private static RulesManagerService create(Context context) {
          RulesManagerServiceHelperImpl helper = new RulesManagerServiceHelperImpl(context);
          File baseVersionFile = new File(TimeZoneDataFiles.getRuntimeModuleTzVersionFile());
          File tzDataDir = new File(TimeZoneDataFiles.getDataTimeZoneRootDir());
          return new RulesManagerService(
                  helper /* permissionHelper */,
                  helper /* executor */,
                  helper /* intentHelper */,
                  PackageTracker.create(context),
                  new TimeZoneDistroInstaller(TAG, baseVersionFile, tzDataDir));
      }
  
      // A constructor that can be used by tests to supply mocked / faked dependencies.
      @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
      RulesManagerService(PermissionHelper permissionHelper, Executor executor,
              RulesManagerIntentHelper intentHelper, PackageTracker packageTracker,
              TimeZoneDistroInstaller timeZoneDistroInstaller) {
          mPermissionHelper = permissionHelper;
          mExecutor = executor;
          mIntentHelper = intentHelper;
          mPackageTracker = packageTracker;
          mInstaller = timeZoneDistroInstaller;
      }
  
      public void start() {
          // Return value deliberately ignored: no action required on failure to start.
          mPackageTracker.start();
      }
  
      @Override // Binder call
      public RulesState getRulesState() {
          mPermissionHelper.enforceCallerHasPermission(REQUIRED_QUERY_PERMISSION);
  
          return getRulesStateInternal();
      }
  
      /** Like {@link #getRulesState()} without the permission check. */
      private RulesState getRulesStateInternal() {
          synchronized(this) {
              TzDataSetVersion baseVersion;
              try {
                  baseVersion = mInstaller.readBaseVersion();
              } catch (IOException e) {
                  Slog.w(TAG, "Failed to read base rules version", e);
                  return null;
              }
  
              // Determine the installed distro state. This should be possible regardless of whether
              // there's an operation in progress.
              DistroVersion installedDistroVersion;
              int distroStatus = DISTRO_STATUS_UNKNOWN;
              DistroRulesVersion installedDistroRulesVersion = null;
              try {
                  installedDistroVersion = mInstaller.getInstalledDistroVersion();
                  if (installedDistroVersion == null) {
                      distroStatus = DISTRO_STATUS_NONE;
                      installedDistroRulesVersion = null;
                  } else {
                      distroStatus = DISTRO_STATUS_INSTALLED;
                      installedDistroRulesVersion = new DistroRulesVersion(
                              installedDistroVersion.rulesVersion,
                              installedDistroVersion.revision);
                  }
              } catch (DistroException | IOException e) {
                  Slog.w(TAG, "Failed to read installed distro.", e);
              }
  
              boolean operationInProgress = this.mOperationInProgress.get();
  
              // Determine the staged operation status, if possible.
              DistroRulesVersion stagedDistroRulesVersion = null;
              int stagedOperationStatus = STAGED_OPERATION_UNKNOWN;
              if (!operationInProgress) {
                  StagedDistroOperation stagedDistroOperation;
                  try {
                      stagedDistroOperation = mInstaller.getStagedDistroOperation();
                      if (stagedDistroOperation == null) {
                          stagedOperationStatus = STAGED_OPERATION_NONE;
                      } else if (stagedDistroOperation.isUninstall) {
                          stagedOperationStatus = STAGED_OPERATION_UNINSTALL;
                      } else {
                          // Must be an install.
                          stagedOperationStatus = STAGED_OPERATION_INSTALL;
                          DistroVersion stagedDistroVersion = stagedDistroOperation.distroVersion;
                          stagedDistroRulesVersion = new DistroRulesVersion(
                                  stagedDistroVersion.rulesVersion,
                                  stagedDistroVersion.revision);
                      }
                  } catch (DistroException | IOException e) {
                      Slog.w(TAG, "Failed to read staged distro.", e);
                  }
              }
              return new RulesState(baseVersion.rulesVersion, DISTRO_FORMAT_VERSION_SUPPORTED,
                      operationInProgress, stagedOperationStatus, stagedDistroRulesVersion,
                      distroStatus, installedDistroRulesVersion);
          }
      }
  
      @Override
      public int requestInstall(ParcelFileDescriptor distroParcelFileDescriptor,
              byte[] checkTokenBytes, ICallback callback) {
  
          boolean closeParcelFileDescriptorOnExit = true;
          try {
              mPermissionHelper.enforceCallerHasPermission(REQUIRED_UPDATER_PERMISSION);
  
              CheckToken checkToken = null;
              if (checkTokenBytes != null) {
                  checkToken = createCheckTokenOrThrow(checkTokenBytes);
              }
              EventLogTags.writeTimezoneRequestInstall(toStringOrNull(checkToken));
  
              synchronized (this) {
                  if (distroParcelFileDescriptor == null) {
                      throw new NullPointerException("distroParcelFileDescriptor == null");
                  }
                  if (callback == null) {
                      throw new NullPointerException("observer == null");
                  }
                  if (mOperationInProgress.get()) {
                      return RulesManager.ERROR_OPERATION_IN_PROGRESS;
                  }
                  mOperationInProgress.set(true);
  
                  // Execute the install asynchronously.
                  mExecutor.execute(
                          new InstallRunnable(distroParcelFileDescriptor, checkToken, callback));
  
                  // The InstallRunnable now owns the ParcelFileDescriptor, so it will close it after
                  // it executes (and we do not have to).
                  closeParcelFileDescriptorOnExit = false;
  
                  return RulesManager.SUCCESS;
              }
          } finally {
              // We should close() the local ParcelFileDescriptor we were passed if it hasn't been
              // passed to another thread to handle.
              if (distroParcelFileDescriptor != null && closeParcelFileDescriptorOnExit) {
                  try {
                      distroParcelFileDescriptor.close();
                  } catch (IOException e) {
                      Slog.w(TAG, "Failed to close distroParcelFileDescriptor", e);
                  }
              }
          }
      }
  
      private class InstallRunnable implements Runnable {
  
          private final ParcelFileDescriptor mDistroParcelFileDescriptor;
          private final CheckToken mCheckToken;
          private final ICallback mCallback;
  
          InstallRunnable(ParcelFileDescriptor distroParcelFileDescriptor, CheckToken checkToken,
                  ICallback callback) {
              mDistroParcelFileDescriptor = distroParcelFileDescriptor;
              mCheckToken = checkToken;
              mCallback = callback;
          }
  
          @Override
          public void run() {
              EventLogTags.writeTimezoneInstallStarted(toStringOrNull(mCheckToken));
  
              boolean success = false;
              // Adopt the ParcelFileDescriptor into this try-with-resources so it is closed
              // when we are done.
              try (ParcelFileDescriptor pfd = mDistroParcelFileDescriptor) {
                  // The ParcelFileDescriptor owns the underlying FileDescriptor and we'll close
                  // it at the end of the try-with-resources.
                  final boolean isFdOwner = false;
                  InputStream is = new FileInputStream(pfd.getFileDescriptor(), isFdOwner);
  
                  TimeZoneDistro distro = new TimeZoneDistro(is);
                  int installerResult = mInstaller.stageInstallWithErrorCode(distro);
  
                  // Notify interested parties that something is staged.
                  sendInstallNotificationIntentIfRequired(installerResult);
  
                  int resultCode = mapInstallerResultToApiCode(installerResult);
                  EventLogTags.writeTimezoneInstallComplete(toStringOrNull(mCheckToken), resultCode);
                  sendFinishedStatus(mCallback, resultCode);
  
                  // All the installer failure modes are currently non-recoverable and won't be
                  // improved by trying again. Therefore success = true.
                  success = true;
              } catch (Exception e) {
                  Slog.w(TAG, "Failed to install distro.", e);
                  EventLogTags.writeTimezoneInstallComplete(
                          toStringOrNull(mCheckToken), Callback.ERROR_UNKNOWN_FAILURE);
                  sendFinishedStatus(mCallback, Callback.ERROR_UNKNOWN_FAILURE);
              } finally {
                  // Notify the package tracker that the operation is now complete.
                  mPackageTracker.recordCheckResult(mCheckToken, success);
  
                  mOperationInProgress.set(false);
              }
          }
  
          private void sendInstallNotificationIntentIfRequired(int installerResult) {
              if (installerResult == TimeZoneDistroInstaller.INSTALL_SUCCESS) {
                  mIntentHelper.sendTimeZoneOperationStaged();
              }
          }
  
          private int mapInstallerResultToApiCode(int installerResult) {
              switch (installerResult) {
                  case TimeZoneDistroInstaller.INSTALL_SUCCESS:
                      return Callback.SUCCESS;
                  case TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE:
                      return Callback.ERROR_INSTALL_BAD_DISTRO_STRUCTURE;
                  case TimeZoneDistroInstaller.INSTALL_FAIL_RULES_TOO_OLD:
                      return Callback.ERROR_INSTALL_RULES_TOO_OLD;
                  case TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_FORMAT_VERSION:
                      return Callback.ERROR_INSTALL_BAD_DISTRO_FORMAT_VERSION;
                  case TimeZoneDistroInstaller.INSTALL_FAIL_VALIDATION_ERROR:
                      return Callback.ERROR_INSTALL_VALIDATION_ERROR;
                  default:
                      return Callback.ERROR_UNKNOWN_FAILURE;
              }
          }
      }
  
      @Override
      public int requestUninstall(byte[] checkTokenBytes, ICallback callback) {
          mPermissionHelper.enforceCallerHasPermission(REQUIRED_UPDATER_PERMISSION);
  
          CheckToken checkToken = null;
          if (checkTokenBytes != null) {
              checkToken = createCheckTokenOrThrow(checkTokenBytes);
          }
          EventLogTags.writeTimezoneRequestUninstall(toStringOrNull(checkToken));
          synchronized(this) {
              if (callback == null) {
                  throw new NullPointerException("callback == null");
              }
  
              if (mOperationInProgress.get()) {
                  return RulesManager.ERROR_OPERATION_IN_PROGRESS;
              }
              mOperationInProgress.set(true);
  
              // Execute the uninstall asynchronously.
              mExecutor.execute(new UninstallRunnable(checkToken, callback));
  
              return RulesManager.SUCCESS;
          }
      }
  
      private class UninstallRunnable implements Runnable {
  
          private final CheckToken mCheckToken;
          private final ICallback mCallback;
  
          UninstallRunnable(CheckToken checkToken, ICallback callback) {
              mCheckToken = checkToken;
              mCallback = callback;
          }
  
          @Override
          public void run() {
              EventLogTags.writeTimezoneUninstallStarted(toStringOrNull(mCheckToken));
              boolean packageTrackerStatus = false;
              try {
                  int uninstallResult = mInstaller.stageUninstall();
  
                  // Notify interested parties that something is staged.
                  sendUninstallNotificationIntentIfRequired(uninstallResult);
  
                  packageTrackerStatus = (uninstallResult == TimeZoneDistroInstaller.UNINSTALL_SUCCESS
                          || uninstallResult == TimeZoneDistroInstaller.UNINSTALL_NOTHING_INSTALLED);
  
                  // Right now we just have Callback.SUCCESS / Callback.ERROR_UNKNOWN_FAILURE for
                  // uninstall. All clients should be checking against SUCCESS. More granular failures
                  // may be added in future.
                  int callbackResultCode =
                          packageTrackerStatus ? Callback.SUCCESS : Callback.ERROR_UNKNOWN_FAILURE;
                  EventLogTags.writeTimezoneUninstallComplete(
                          toStringOrNull(mCheckToken), callbackResultCode);
                  sendFinishedStatus(mCallback, callbackResultCode);
              } catch (Exception e) {
                  EventLogTags.writeTimezoneUninstallComplete(
                          toStringOrNull(mCheckToken), Callback.ERROR_UNKNOWN_FAILURE);
                  Slog.w(TAG, "Failed to uninstall distro.", e);
                  sendFinishedStatus(mCallback, Callback.ERROR_UNKNOWN_FAILURE);
              } finally {
                  // Notify the package tracker that the operation is now complete.
                  mPackageTracker.recordCheckResult(mCheckToken, packageTrackerStatus);
  
                  mOperationInProgress.set(false);
              }
          }
  
          private void sendUninstallNotificationIntentIfRequired(int uninstallResult) {
              switch (uninstallResult) {
                  case TimeZoneDistroInstaller.UNINSTALL_SUCCESS:
                      mIntentHelper.sendTimeZoneOperationStaged();
                      break;
                  case TimeZoneDistroInstaller.UNINSTALL_NOTHING_INSTALLED:
                      mIntentHelper.sendTimeZoneOperationUnstaged();
                      break;
                  case TimeZoneDistroInstaller.UNINSTALL_FAIL:
                  default:
                      // No-op - unknown or nothing to notify about.
              }
          }
      }
  
      private void sendFinishedStatus(ICallback callback, int resultCode) {
          try {
              callback.onFinished(resultCode);
          } catch (RemoteException e) {
              Slog.e(TAG, "Unable to notify observer of result", e);
          }
      }
  
      @Override
      public void requestNothing(byte[] checkTokenBytes, boolean success) {
          mPermissionHelper.enforceCallerHasPermission(REQUIRED_UPDATER_PERMISSION);
          CheckToken checkToken = null;
          if (checkTokenBytes != null) {
              checkToken = createCheckTokenOrThrow(checkTokenBytes);
          }
          EventLogTags.writeTimezoneRequestNothing(toStringOrNull(checkToken));
          mPackageTracker.recordCheckResult(checkToken, success);
          EventLogTags.writeTimezoneNothingComplete(toStringOrNull(checkToken));
      }
  
      @Override
      protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
          if (!mPermissionHelper.checkDumpPermission(TAG, pw)) {
              return;
          }
  
          RulesState rulesState = getRulesStateInternal();
          if (args != null && args.length == 2) {
              // Formatting options used for automated tests. The format is less free-form than
              // the -format options, which are intended to be easier to parse.
              if ("-format_state".equals(args[0]) && args[1] != null) {
                  for (char c : args[1].toCharArray()) {
                      switch (c) {
                          case 'p': {
                              // Report operation in progress
                              String value = "Unknown";
                              if (rulesState != null) {
                                  value = Boolean.toString(rulesState.isOperationInProgress());
                              }
                              pw.println("Operation in progress: " + value);
                              break;
                          }
                          case 'b': {
                              // Report base rules version
                              String value = "Unknown";
                              if (rulesState != null) {
                                  value = rulesState.getBaseRulesVersion();
                              }
                              pw.println("Base rules version: " + value);
                              break;
                          }
                          case 'c': {
                              // Report current installation state
                              String value = "Unknown";
                              if (rulesState != null) {
                                  value = distroStatusToString(rulesState.getDistroStatus());
                              }
                              pw.println("Current install state: " + value);
                              break;
                          }
                          case 'i': {
                              // Report currently installed version
                              String value = "Unknown";
                              if (rulesState != null) {
                                  DistroRulesVersion installedRulesVersion =
                                          rulesState.getInstalledDistroRulesVersion();
                                  if (installedRulesVersion == null) {
                                      value = "<None>";
                                  } else {
                                      value = installedRulesVersion.toDumpString();
                                  }
                              }
                              pw.println("Installed rules version: " + value);
                              break;
                          }
                          case 'o': {
                              // Report staged operation type
                              String value = "Unknown";
                              if (rulesState != null) {
                                  int stagedOperationType = rulesState.getStagedOperationType();
                                  value = stagedOperationToString(stagedOperationType);
                              }
                              pw.println("Staged operation: " + value);
                              break;
                          }
                          case 't': {
                              // Report staged version (i.e. the one that will be installed next boot
                              // if the staged operation is an install).
                              String value = "Unknown";
                              if (rulesState != null) {
                                  DistroRulesVersion stagedDistroRulesVersion =
                                          rulesState.getStagedDistroRulesVersion();
                                  if (stagedDistroRulesVersion == null) {
                                      value = "<None>";
                                  } else {
                                      value = stagedDistroRulesVersion.toDumpString();
                                  }
                              }
                              pw.println("Staged rules version: " + value);
                              break;
                          }
                          case 'a': {
                              // Report the active rules version (i.e. the rules in use by the current
                              // process).
                              pw.println("Active rules version (ICU, ZoneInfoDB, TimeZoneFinder): "
                                      + ICU.getTZDataVersion() + ","
                                      + ZoneInfoDB.getInstance().getVersion() + ","
                                      + TimeZoneFinder.getInstance().getIanaVersion());
                              break;
                          }
                          default: {
                              pw.println("Unknown option: " + c);
                          }
                      }
                  }
                  return;
              }
          }
  
          pw.println("RulesManagerService state: " + toString());
          pw.println("Active rules version (ICU, ZoneInfoDB, TimeZoneFinder): "
                  + ICU.getTZDataVersion() + ","
                  + ZoneInfoDB.getInstance().getVersion() + ","
                  + TimeZoneFinder.getInstance().getIanaVersion());
          pw.println("Distro state: " + rulesState.toString());
          mPackageTracker.dump(pw);
      }
  
      /**
       * Called when the device is considered idle.
       */
      void notifyIdle() {
          // No package has changed: we are just triggering because the device is idle and there
          // *might* be work to do.
          final boolean packageChanged = false;
          mPackageTracker.triggerUpdateIfNeeded(packageChanged);
      }
  
      @Override
      public String toString() {
          return "RulesManagerService{" +
                  "mOperationInProgress=" + mOperationInProgress +
                  '}';
      }
  
      private static CheckToken createCheckTokenOrThrow(byte[] checkTokenBytes) {
          CheckToken checkToken;
          try {
              checkToken = CheckToken.fromByteArray(checkTokenBytes);
          } catch (IOException e) {
              throw new IllegalArgumentException("Unable to read token bytes "
                      + Arrays.toString(checkTokenBytes), e);
          }
          return checkToken;
      }
  
      private static String distroStatusToString(int distroStatus) {
          switch(distroStatus) {
              case DISTRO_STATUS_NONE:
                  return "None";
              case DISTRO_STATUS_INSTALLED:
                  return "Installed";
              case DISTRO_STATUS_UNKNOWN:
              default:
                  return "Unknown";
          }
      }
  
      private static String stagedOperationToString(int stagedOperationType) {
          switch(stagedOperationType) {
              case STAGED_OPERATION_NONE:
                  return "None";
              case STAGED_OPERATION_UNINSTALL:
                  return "Uninstall";
              case STAGED_OPERATION_INSTALL:
                  return "Install";
              case STAGED_OPERATION_UNKNOWN:
              default:
                  return "Unknown";
          }
      }
  
      private static String toStringOrNull(Object obj) {
          return obj == null ? null : obj.toString();
      }
  }
  