blob: bbd1ae60cb629548d3bf2ee3432b696cf3964f5e [file] [log] [blame]
/*
* 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.icu.util.TimeZone;
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.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.getTimeZoneModuleTzVersionFile());
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.getRulesVersion(), 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): "
+ TimeZone.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): "
+ TimeZone.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();
}
}