Merge "Convert libhwui to Android.bp"
diff --git a/Android.mk b/Android.mk
index 620b7a5..02caa0a 100644
--- a/Android.mk
+++ b/Android.mk
@@ -103,6 +103,8 @@
 	core/java/android/app/backup/IFullBackupRestoreObserver.aidl \
 	core/java/android/app/backup/IRestoreObserver.aidl \
 	core/java/android/app/backup/IRestoreSession.aidl \
+	core/java/android/app/timezone/ICallback.aidl \
+	core/java/android/app/timezone/IRulesManager.aidl \
 	core/java/android/app/usage/IUsageStatsManager.aidl \
 	core/java/android/bluetooth/IBluetooth.aidl \
 	core/java/android/bluetooth/IBluetoothA2dp.aidl \
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index ce019cac..1e5ea26 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -26,6 +26,7 @@
 import android.app.admin.DevicePolicyManager;
 import android.app.job.IJobScheduler;
 import android.app.job.JobScheduler;
+import android.app.timezone.RulesManager;
 import android.app.trust.TrustManager;
 import android.app.usage.IUsageStatsManager;
 import android.app.usage.NetworkStatsManager;
@@ -786,6 +787,13 @@
                 return new ContextHubManager(ctx.getOuterContext(),
                   ctx.mMainThread.getHandler().getLooper());
             }});
+
+        registerService(Context.TIME_ZONE_RULES_MANAGER_SERVICE, RulesManager.class,
+                new CachedServiceFetcher<RulesManager>() {
+            @Override
+            public RulesManager createService(ContextImpl ctx) {
+                return new RulesManager(ctx.getOuterContext());
+            }});
     }
 
     /**
diff --git a/core/java/android/app/timezone/Callback.java b/core/java/android/app/timezone/Callback.java
new file mode 100644
index 0000000..b51e5ba
--- /dev/null
+++ b/core/java/android/app/timezone/Callback.java
@@ -0,0 +1,76 @@
+/*
+ * 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 android.app.timezone;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Callback interface for receiving information about an async time zone operation.
+ * The methods will be called on your application's main thread.
+ *
+ * @hide
+ */
+// TODO(nfuller): Expose necessary APIs for OEMs with @SystemApi. http://b/31008728
+public abstract class Callback {
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({SUCCESS, ERROR_UNKNOWN_FAILURE, ERROR_INSTALL_BAD_DISTRO_STRUCTURE,
+        ERROR_INSTALL_BAD_DISTRO_FORMAT_VERSION, ERROR_INSTALL_RULES_TOO_OLD,
+        ERROR_INSTALL_VALIDATION_ERROR})
+    public @interface AsyncResultCode {}
+
+    /**
+     * Indicates that an operation succeeded.
+     */
+    public static final int SUCCESS = 0;
+
+    /**
+     * Indicates an install / uninstall did not fully succeed for an unknown reason.
+     */
+    public static final int ERROR_UNKNOWN_FAILURE = 1;
+
+    /**
+     * Indicates an install failed because of a structural issue with the provided distro,
+     * e.g. it wasn't in the right format or the contents were structured incorrectly.
+     */
+    public static final int ERROR_INSTALL_BAD_DISTRO_STRUCTURE = 2;
+
+    /**
+     * Indicates an install failed because of a versioning issue with the provided distro,
+     * e.g. it was created for a different version of Android.
+     */
+    public static final int ERROR_INSTALL_BAD_DISTRO_FORMAT_VERSION = 3;
+
+    /**
+     * Indicates an install failed because the rules provided are too old for the device,
+     * e.g. the Android device shipped with a newer rules version.
+     */
+    public static final int ERROR_INSTALL_RULES_TOO_OLD = 4;
+
+    /**
+     * Indicates an install failed because the distro contents failed validation.
+     */
+    public static final int ERROR_INSTALL_VALIDATION_ERROR = 5;
+
+    /**
+     * Reports the result of an async time zone operation.
+     */
+    public abstract void onFinished(@AsyncResultCode int status);
+}
diff --git a/core/java/android/app/timezone/DistroFormatVersion.java b/core/java/android/app/timezone/DistroFormatVersion.java
new file mode 100644
index 0000000..e879e8f
--- /dev/null
+++ b/core/java/android/app/timezone/DistroFormatVersion.java
@@ -0,0 +1,120 @@
+/*
+ * 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 android.app.timezone;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Versioning information about a distro's format or a device's supported format.
+ *
+ * <p>The following properties are included:
+ * <dl>
+ *     <dt>majorVersion</dt>
+ *     <dd>the major distro format version. Major versions differences are not compatible - e.g.
+ *     2 is not compatible with 1 or 3.</dd>
+ *     <dt>minorVersion</dt>
+ *     <dd>the minor distro format version. Minor versions should be backwards compatible iff the
+ *     major versions match exactly, i.e. version 2.2 will be compatible with 2.1 devices but not
+ *     2.3 devices.</dd>
+ * </dl>
+ *
+ * @hide
+ */
+// TODO(nfuller): Expose necessary APIs for OEMs with @SystemApi. http://b/31008728
+public final class DistroFormatVersion implements Parcelable {
+
+    private final int mMajorVersion;
+    private final int mMinorVersion;
+
+    public DistroFormatVersion(int majorVersion, int minorVersion) {
+        mMajorVersion = Utils.validateVersion("major", majorVersion);
+        mMinorVersion = Utils.validateVersion("minor", minorVersion);
+    }
+
+    public static final Creator<DistroFormatVersion> CREATOR = new Creator<DistroFormatVersion>() {
+        public DistroFormatVersion createFromParcel(Parcel in) {
+            int majorVersion = in.readInt();
+            int minorVersion = in.readInt();
+            return new DistroFormatVersion(majorVersion, minorVersion);
+        }
+
+        public DistroFormatVersion[] newArray(int size) {
+            return new DistroFormatVersion[size];
+        }
+    };
+
+    public int getMajorVersion() {
+        return mMajorVersion;
+    }
+
+    public int getMinorVersion() {
+        return mMinorVersion;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(mMajorVersion);
+        out.writeInt(mMinorVersion);
+    }
+
+    /**
+     * If this object describes a device's supported version and the parameter describes a distro's
+     * version, this method returns whether the device would accept the distro.
+     */
+    public boolean supports(DistroFormatVersion distroFormatVersion) {
+        return mMajorVersion == distroFormatVersion.mMajorVersion
+                && mMinorVersion <= distroFormatVersion.mMinorVersion;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        DistroFormatVersion that = (DistroFormatVersion) o;
+
+        if (mMajorVersion != that.mMajorVersion) {
+            return false;
+        }
+        return mMinorVersion == that.mMinorVersion;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = mMajorVersion;
+        result = 31 * result + mMinorVersion;
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "DistroFormatVersion{"
+                + "mMajorVersion=" + mMajorVersion
+                + ", mMinorVersion=" + mMinorVersion
+                + '}';
+    }
+}
diff --git a/core/java/android/app/timezone/DistroRulesVersion.java b/core/java/android/app/timezone/DistroRulesVersion.java
new file mode 100644
index 0000000..5503ce1
--- /dev/null
+++ b/core/java/android/app/timezone/DistroRulesVersion.java
@@ -0,0 +1,128 @@
+/*
+ * 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 android.app.timezone;
+
+import static android.app.timezone.Utils.validateRulesVersion;
+import static android.app.timezone.Utils.validateVersion;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Versioning information about a set of time zone rules.
+ *
+ * <p>The following properties are included:
+ * <dl>
+ *     <dt>rulesVersion</dt>
+ *     <dd>the IANA rules version. e.g. "2017a"</dd>
+ *     <dt>revision</dt>
+ *     <dd>the revision for the rules. Allows there to be several revisions for a given IANA rules
+ *     release. Numerically higher is newer.</dd>
+ * </dl>
+ *
+ * @hide
+ */
+// TODO(nfuller): Expose necessary APIs for OEMs with @SystemApi. http://b/31008728
+public final class DistroRulesVersion implements Parcelable {
+
+    private final String mRulesVersion;
+    private final int mRevision;
+
+    public DistroRulesVersion(String rulesVersion, int revision) {
+        mRulesVersion = validateRulesVersion("rulesVersion", rulesVersion);
+        mRevision = validateVersion("revision", revision);
+    }
+
+    public static final Creator<DistroRulesVersion> CREATOR = new Creator<DistroRulesVersion>() {
+        public DistroRulesVersion createFromParcel(Parcel in) {
+            String rulesVersion = in.readString();
+            int revision = in.readInt();
+            return new DistroRulesVersion(rulesVersion, revision);
+        }
+
+        public DistroRulesVersion[] newArray(int size) {
+            return new DistroRulesVersion[size];
+        }
+    };
+
+    public String getRulesVersion() {
+        return mRulesVersion;
+    }
+
+    public int getRevision() {
+        return mRevision;
+    }
+
+    /**
+     * Returns true if this DistroRulesVersion is older than the one supplied. It returns false if
+     * it is the same or newer. This method compares the {@code rulesVersion} and the
+     * {@code revision}.
+     */
+    public boolean isOlderThan(DistroRulesVersion distroRulesVersion) {
+        int rulesComparison = mRulesVersion.compareTo(distroRulesVersion.mRulesVersion);
+        if (rulesComparison < 0) {
+            return true;
+        }
+        if (rulesComparison > 0) {
+            return false;
+        }
+        return mRevision < distroRulesVersion.mRevision;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeString(mRulesVersion);
+        out.writeInt(mRevision);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        DistroRulesVersion that = (DistroRulesVersion) o;
+
+        if (mRevision != that.mRevision) {
+            return false;
+        }
+        return mRulesVersion.equals(that.mRulesVersion);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = mRulesVersion.hashCode();
+        result = 31 * result + mRevision;
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "DistroRulesVersion{"
+                + "mRulesVersion='" + mRulesVersion + '\''
+                + ", mRevision='" + mRevision + '\''
+                + '}';
+    }
+}
diff --git a/core/java/android/app/timezone/ICallback.aidl b/core/java/android/app/timezone/ICallback.aidl
new file mode 100644
index 0000000..519ef1a
--- /dev/null
+++ b/core/java/android/app/timezone/ICallback.aidl
@@ -0,0 +1,27 @@
+/*
+ * 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 android.app.timezone;
+
+/**
+ * Callback interface for a timezone updater to receive information about the success or failure of
+ * an installation/uninstallation attempt.
+ *
+ * {@hide}
+ */
+oneway interface ICallback {
+    void onFinished(int error);
+}
\ No newline at end of file
diff --git a/core/java/android/app/timezone/IRulesManager.aidl b/core/java/android/app/timezone/IRulesManager.aidl
new file mode 100644
index 0000000..40f3fd2
--- /dev/null
+++ b/core/java/android/app/timezone/IRulesManager.aidl
@@ -0,0 +1,84 @@
+/*
+ * 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 android.app.timezone;
+
+import android.app.timezone.ICallback;
+import android.app.timezone.RulesState;
+import android.os.ParcelFileDescriptor;
+
+ /**
+  * Interface to the TimeZone Rules Manager Service.
+  *
+  * <p>This interface is only intended for system apps to call. They should use the
+  * {@link android.app.timezone.RulesManager} class rather than going through this
+  * Binder interface directly. See {@link android.app.timezone.RulesManager} for more complete
+  * documentation.
+  *
+  * {@hide}
+  */
+interface IRulesManager {
+
+    /**
+     * Returns information about the current time zone rules state such as the IANA version of
+     * the system and any currently installed distro. This method is intended to allow clients to
+     * determine if the current state can be improved; for example by passing the information to a
+     * server that may provide a new distro for download.
+     */
+    RulesState getRulesState();
+
+    /**
+     * Requests installation of the supplied distro. The distro must have been checked for integrity
+     * by the caller or have been received via a trusted mechanism.
+     *
+     * @param distroFileDescriptor the file descriptor for the distro
+     * @param checkToken an optional token provided if the install was triggered in response to a
+     *     {@link RulesUpdaterContract#ACTION_TRIGGER_RULES_UPDATE_CHECK} intent
+     * @param callback the {@link ICallback} to receive callbacks related to the
+     *     installation
+     * @return zero if the installation will be attempted; nonzero on error
+     */
+    int requestInstall(in ParcelFileDescriptor distroFileDescriptor, in byte[] checkToken,
+            ICallback callback);
+
+    /**
+     * Requests uninstallation of the currently installed distro (leaving the device with no
+     * distro installed).
+     *
+     * @param checkToken an optional token provided if the uninstall was triggered in response to a
+     *     {@link RulesUpdaterContract#ACTION_TRIGGER_RULES_UPDATE_CHECK} intent
+     * @param callback the {@link ICallback} to receive callbacks related to the
+     *     uninstall
+     * @return zero if the uninstallation will be attempted; nonzero on error
+     */
+    int requestUninstall(in byte[] checkToken, ICallback callback);
+
+    /**
+     * Requests the system does not modify the currently installed time zone distro, if any. This
+     * method records the fact that a time zone check operation triggered by the system is now
+     * complete and there was nothing to do. The token passed should be the one presented when the
+     * check was triggered.
+     *
+     * <p>Note: Passing {@code success == false} may result in more checks being triggered. Clients
+     * should be careful not to pass false if the failure is unlikely to resolve by itself.
+     *
+     * @param checkToken an optional token provided if the install was triggered in response to a
+     *     {@link RulesUpdaterContract#ACTION_TRIGGER_RULES_UPDATE_CHECK} intent
+     * @param success true if the check was successful, false if it was not successful but may
+     *     succeed if it is retried
+     */
+    void requestNothing(in byte[] token, boolean success);
+}
diff --git a/core/java/android/app/timezone/RulesManager.java b/core/java/android/app/timezone/RulesManager.java
new file mode 100644
index 0000000..649d894
--- /dev/null
+++ b/core/java/android/app/timezone/RulesManager.java
@@ -0,0 +1,212 @@
+/*
+ * 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 android.app.timezone;
+
+import android.annotation.IntDef;
+import android.content.Context;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+
+/**
+ * The interface through which a time zone update application interacts with the Android system
+ * to handle time zone rule updates.
+ *
+ * <p>This interface is intended for use with the default APK-based time zone rules update
+ * application but it can also be used by OEMs if that mechanism is turned off using configuration.
+ * All callers must possess the {@link android.Manifest.permission#UPDATE_TIME_ZONE_RULES} system
+ * permission.
+ *
+ * <p>When using the default mechanism, when properly configured the Android system will send a
+ * {@link RulesUpdaterContract#ACTION_TRIGGER_RULES_UPDATE_CHECK} intent with a
+ * {@link RulesUpdaterContract#EXTRA_CHECK_TOKEN} extra to the time zone rules updater application
+ * when it detects that it or the OEM's APK containing time zone rules data has been modified. The
+ * updater application is then responsible for calling one of
+ * {@link #requestInstall(ParcelFileDescriptor, byte[], Callback)},
+ * {@link #requestUninstall(byte[], Callback)} or
+ * {@link #requestNothing(byte[], boolean)}, indicating, respectively, whether a new time zone rules
+ * distro should be installed, the current distro should be uninstalled, or there is nothing to do
+ * (or that the correct operation could not be determined due to an error). In each case the updater
+ * must pass the {@link RulesUpdaterContract#EXTRA_CHECK_TOKEN} value it received from the intent
+ * back so the system in the {@code checkToken} parameter.
+ *
+ * <p>If OEMs want to handle their own time zone rules updates, perhaps via a server-side component
+ * rather than an APK, then they should disable the default triggering mechanism in config and are
+ * responsible for triggering their own update checks / installs / uninstalls. In this case the
+ * "check token" parameter can be left null and there is never any need to call
+ * {@link #requestNothing(byte[], boolean)}.
+ *
+ * <p>OEMs should not mix the default mechanism and their own as this could lead to conflicts and
+ * unnecessary checks being triggered.
+ *
+ * <p>Applications obtain this using {@link android.app.Activity#getSystemService(String)} with
+ * {@link Context#TIME_ZONE_RULES_MANAGER_SERVICE}.
+ * @hide
+ */
+// TODO(nfuller): Expose necessary APIs for OEMs with @SystemApi. http://b/31008728
+public final class RulesManager {
+    private static final String TAG = "timezone.RulesManager";
+    private static final boolean DEBUG = false;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({SUCCESS, ERROR_UNKNOWN_FAILURE, ERROR_OPERATION_IN_PROGRESS})
+    public @interface ResultCode {}
+
+    /**
+     * Indicates that an operation succeeded.
+     */
+    public static final int SUCCESS = 0;
+
+    /**
+     * Indicates that an install/uninstall cannot be initiated because there is one already in
+     * progress.
+     */
+    public static final int ERROR_OPERATION_IN_PROGRESS = 1;
+
+    /**
+     * Indicates an install / uninstall did not fully succeed for an unknown reason.
+     */
+    public static final int ERROR_UNKNOWN_FAILURE = 2;
+
+    private final Context mContext;
+    private final IRulesManager mIRulesManager;
+
+    public RulesManager(Context context) {
+        mContext = context;
+        mIRulesManager = IRulesManager.Stub.asInterface(
+                ServiceManager.getService(Context.TIME_ZONE_RULES_MANAGER_SERVICE));
+    }
+
+    /**
+     * Returns information about the current time zone rules state such as the IANA version of
+     * the system and any currently installed distro. This method is intended to allow clients to
+     * determine if the current state can be improved; for example by passing the information to a
+     * server that may provide a new distro for download.
+     */
+    public RulesState getRulesState() {
+        try {
+            logDebug("sIRulesManager.getRulesState()");
+            RulesState rulesState = mIRulesManager.getRulesState();
+            logDebug("sIRulesManager.getRulesState() returned " + rulesState);
+            return rulesState;
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Requests installation of the supplied distro. The distro must have been checked for integrity
+     * by the caller or have been received via a trusted mechanism.
+     *
+     * @param distroFileDescriptor the file descriptor for the distro
+     * @param checkToken an optional token provided if the install was triggered in response to a
+     *     {@link RulesUpdaterContract#ACTION_TRIGGER_RULES_UPDATE_CHECK} intent
+     * @param callback the {@link Callback} to receive callbacks related to the installation
+     * @return {@link #SUCCESS} if the installation will be attempted
+     */
+    @ResultCode
+    public int requestInstall(
+            ParcelFileDescriptor distroFileDescriptor, byte[] checkToken, Callback callback)
+            throws IOException {
+
+        ICallback iCallback = new CallbackWrapper(mContext, callback);
+        try {
+            logDebug("sIRulesManager.requestInstall()");
+            return mIRulesManager.requestInstall(distroFileDescriptor, checkToken, iCallback);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Requests uninstallation of the currently installed distro (leaving the device with no
+     * distro installed).
+     *
+     * @param checkToken an optional token provided if the uninstall was triggered in response to a
+     *     {@link RulesUpdaterContract#ACTION_TRIGGER_RULES_UPDATE_CHECK} intent
+     * @param callback the {@link Callback} to receive callbacks related to the uninstall
+     * @return {@link #SUCCESS} if the uninstallation will be attempted
+     */
+    @ResultCode
+    public int requestUninstall(byte[] checkToken, Callback callback) {
+        ICallback iCallback = new CallbackWrapper(mContext, callback);
+        try {
+            logDebug("sIRulesManager.requestUninstall()");
+            return mIRulesManager.requestUninstall(checkToken, iCallback);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /*
+     * We wrap incoming binder calls with a private class implementation that
+     * redirects them into main-thread actions.  This serializes the backup
+     * progress callbacks nicely within the usual main-thread lifecycle pattern.
+     */
+    private class CallbackWrapper extends ICallback.Stub {
+        final Handler mHandler;
+        final Callback mCallback;
+
+        CallbackWrapper(Context context, Callback callback) {
+            mCallback = callback;
+            mHandler = new Handler(context.getMainLooper());
+        }
+
+        // Binder calls into this object just enqueue on the main-thread handler
+        @Override
+        public void onFinished(int status) {
+            logDebug("mCallback.onFinished(status), status=" + status);
+            mHandler.post(() -> mCallback.onFinished(status));
+        }
+    }
+
+    /**
+     * Requests the system does not modify the currently installed time zone distro, if any. This
+     * method records the fact that a time zone check operation triggered by the system is now
+     * complete and there was nothing to do. The token passed should be the one presented when the
+     * check was triggered.
+     *
+     * <p>Note: Passing {@code success == false} may result in more checks being triggered. Clients
+     * should be careful not to pass false if the failure is unlikely to resolve by itself.
+     *
+     * @param checkToken an optional token provided if the install was triggered in response to a
+     *     {@link RulesUpdaterContract#ACTION_TRIGGER_RULES_UPDATE_CHECK} intent
+     * @param succeeded true if the check was successful, false if it was not successful but may
+     *     succeed if it is retried
+     */
+    public void requestNothing(byte[] checkToken, boolean succeeded) {
+        try {
+            logDebug("sIRulesManager.requestNothing() with token=" + Arrays.toString(checkToken));
+            mIRulesManager.requestNothing(checkToken, succeeded);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    static void logDebug(String msg) {
+        if (DEBUG) {
+            Log.v(TAG, msg);
+        }
+    }
+}
diff --git a/core/java/android/app/timezone/RulesState.aidl b/core/java/android/app/timezone/RulesState.aidl
new file mode 100644
index 0000000..f789120
--- /dev/null
+++ b/core/java/android/app/timezone/RulesState.aidl
@@ -0,0 +1,17 @@
+/*
+ * 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.
+ */
+
+parcelable RulesState;
\ No newline at end of file
diff --git a/core/java/android/app/timezone/RulesState.java b/core/java/android/app/timezone/RulesState.java
new file mode 100644
index 0000000..33f4e80
--- /dev/null
+++ b/core/java/android/app/timezone/RulesState.java
@@ -0,0 +1,319 @@
+/*
+ * 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 android.app.timezone;
+
+import static android.app.timezone.Utils.validateConditionalNull;
+import static android.app.timezone.Utils.validateNotNull;
+import static android.app.timezone.Utils.validateRulesVersion;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Description of the state of time zone rules on a device.
+ *
+ * <p>The following properties are included:
+ * <dl>
+ *     <dt>systemRulesVersion</dt>
+ *     <dd>the IANA rules version that shipped with the OS. Always present. e.g. "2017a".</dd>
+ *     <dt>distroFormatVersionSupported</dt>
+ *     <dd>the distro format version supported by this device. Always present.</dd>
+ *     <dt>operationInProgress</dt>
+ *     <dd>{@code true} if there is an install / uninstall operation currently happening.</dd>
+ *     <dt>stagedOperationType</dt>
+ *     <dd>one of {@link #STAGED_OPERATION_UNKNOWN}, {@link #STAGED_OPERATION_NONE},
+ *     {@link #STAGED_OPERATION_UNINSTALL} and {@link #STAGED_OPERATION_INSTALL} indicating whether
+ *     there is a currently staged time zone distro operation. {@link #STAGED_OPERATION_UNKNOWN} is
+ *     used when {@link #isOperationInProgress()} is {@code true}. Staged operations currently
+ *     require a reboot to become active.</dd>
+ *     <dt>stagedDistroRulesVersion</dt>
+ *     <dd>[present if distroStagedState == STAGED_STATE_INSTALL], the rules version of the distro
+ *     currently staged for installation.</dd>
+ *     <dt>distroStatus</dt>
+ *     <dd>{@link #DISTRO_STATUS_INSTALLED} if there is a time zone distro installed and active,
+ *     {@link #DISTRO_STATUS_NONE} if there is no active installed distro.
+ *     {@link #DISTRO_STATUS_UNKNOWN} is used when {@link #isOperationInProgress()} is {@code true}.
+ *     </dd>
+ *     <dt>installedDistroRulesVersion</dt>
+ *     <dd>[present if distroStatus == {@link #DISTRO_STATUS_INSTALLED}], the rules version of the
+ *     installed and active distro.</dd>
+ * </dl>
+ *
+ * @hide
+ */
+// TODO(nfuller): Expose necessary APIs for OEMs with @SystemApi. http://b/31008728
+public final class RulesState implements Parcelable {
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            STAGED_OPERATION_UNKNOWN,
+            STAGED_OPERATION_NONE,
+            STAGED_OPERATION_UNINSTALL,
+            STAGED_OPERATION_INSTALL })
+    private @interface StagedOperationType {}
+
+    /** Staged state could not be determined. */
+    public static final int STAGED_OPERATION_UNKNOWN = 0;
+    /** Nothing is staged. */
+    public static final int STAGED_OPERATION_NONE = 1;
+    /** An uninstall is staged. */
+    public static final int STAGED_OPERATION_UNINSTALL = 2;
+    /** An install is staged. */
+    public static final int STAGED_OPERATION_INSTALL = 3;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            DISTRO_STATUS_UNKNOWN,
+            DISTRO_STATUS_NONE,
+            DISTRO_STATUS_INSTALLED })
+    private @interface DistroStatus {}
+
+    /** The current distro status could not be determined. */
+    public static final int DISTRO_STATUS_UNKNOWN = 0;
+    /** There is no active installed time zone distro. */
+    public static final int DISTRO_STATUS_NONE = 1;
+    /** The is an active, installed time zone distro. */
+    public static final int DISTRO_STATUS_INSTALLED = 2;
+
+    private static final byte BYTE_FALSE = 0;
+    private static final byte BYTE_TRUE = 1;
+
+    private final String mSystemRulesVersion;
+    private final DistroFormatVersion mDistroFormatVersionSupported;
+    private final boolean mOperationInProgress;
+    @StagedOperationType private final int mStagedOperationType;
+    @Nullable private final DistroRulesVersion mStagedDistroRulesVersion;
+    @DistroStatus private final int mDistroStatus;
+    @Nullable private final DistroRulesVersion mInstalledDistroRulesVersion;
+
+    public RulesState(String systemRulesVersion, DistroFormatVersion distroFormatVersionSupported,
+            boolean operationInProgress,
+            @StagedOperationType int stagedOperationType,
+            @Nullable DistroRulesVersion stagedDistroRulesVersion,
+            @DistroStatus int distroStatus,
+            @Nullable DistroRulesVersion installedDistroRulesVersion) {
+        this.mSystemRulesVersion = validateRulesVersion("systemRulesVersion", systemRulesVersion);
+        this.mDistroFormatVersionSupported =
+                validateNotNull("distroFormatVersionSupported", distroFormatVersionSupported);
+        this.mOperationInProgress = operationInProgress;
+
+        if (operationInProgress && stagedOperationType != STAGED_OPERATION_UNKNOWN) {
+            throw new IllegalArgumentException(
+                    "stagedOperationType != STAGED_OPERATION_UNKNOWN");
+        }
+        this.mStagedOperationType = validateStagedOperation(stagedOperationType);
+        this.mStagedDistroRulesVersion = validateConditionalNull(
+                mStagedOperationType == STAGED_OPERATION_INSTALL /* requireNotNull */,
+                "stagedDistroRulesVersion", stagedDistroRulesVersion);
+
+        if (operationInProgress && distroStatus != DISTRO_STATUS_UNKNOWN) {
+            throw new IllegalArgumentException("distroInstalled != DISTRO_STATUS_UNKNOWN");
+        }
+        this.mDistroStatus = validateDistroStatus(distroStatus);
+        this.mInstalledDistroRulesVersion = validateConditionalNull(
+                mDistroStatus == DISTRO_STATUS_INSTALLED/* requireNotNull */,
+                "installedDistroRulesVersion", installedDistroRulesVersion);
+    }
+
+    public String getSystemRulesVersion() {
+        return mSystemRulesVersion;
+    }
+
+    public boolean isOperationInProgress() {
+        return mOperationInProgress;
+    }
+
+    public @StagedOperationType int getStagedOperationType() {
+        return mStagedOperationType;
+    }
+
+    /**
+     * Returns the staged rules version when {@link #getStagedOperationType()} is
+     * {@link #STAGED_OPERATION_INSTALL}.
+     */
+    public @Nullable DistroRulesVersion getStagedDistroRulesVersion() {
+        return mStagedDistroRulesVersion;
+    }
+
+    public @DistroStatus int getDistroStatus() {
+        return mDistroStatus;
+    }
+
+    /**
+     * Returns the installed rules version when {@link #getDistroStatus()} is
+     * {@link #DISTRO_STATUS_INSTALLED}.
+     */
+    public @Nullable DistroRulesVersion getInstalledDistroRulesVersion() {
+        return mInstalledDistroRulesVersion;
+    }
+
+    /**
+     * Returns true if a distro in the specified format is supported on this device.
+     */
+    public boolean isDistroFormatVersionSupported(DistroFormatVersion distroFormatVersion) {
+        return mDistroFormatVersionSupported.supports(distroFormatVersion);
+    }
+
+    /**
+     * Returns true if the distro IANA rules version supplied is newer or the same as the version in
+     * the system image data files.
+     */
+    public boolean isSystemVersionOlderThan(DistroRulesVersion distroRulesVersion) {
+        return mSystemRulesVersion.compareTo(distroRulesVersion.getRulesVersion()) < 0;
+    }
+
+    public boolean isDistroInstalled() {
+        return mDistroStatus == DISTRO_STATUS_INSTALLED;
+    }
+
+    /**
+     * Returns true if the rules version supplied is newer than the one currently installed. If
+     * there is no installed distro this method throws IllegalStateException.
+     */
+    public boolean isInstalledDistroOlderThan(DistroRulesVersion distroRulesVersion) {
+        if (mOperationInProgress) {
+            throw new IllegalStateException("Distro state not known: operation in progress.");
+        }
+        if (!isDistroInstalled()) {
+            throw new IllegalStateException("No distro installed.");
+        }
+        return mInstalledDistroRulesVersion.isOlderThan(distroRulesVersion);
+    }
+
+    public static final Parcelable.Creator<RulesState> CREATOR =
+            new Parcelable.Creator<RulesState>() {
+        public RulesState createFromParcel(Parcel in) {
+            return RulesState.createFromParcel(in);
+        }
+
+        public RulesState[] newArray(int size) {
+            return new RulesState[size];
+        }
+    };
+
+    private static RulesState createFromParcel(Parcel in) {
+        String systemRulesVersion = in.readString();
+        DistroFormatVersion distroFormatVersionSupported = in.readParcelable(null);
+        boolean operationInProgress = in.readByte() == BYTE_TRUE;
+        int distroStagedState = in.readByte();
+        DistroRulesVersion stagedDistroRulesVersion = in.readParcelable(null);
+        int installedDistroStatus = in.readByte();
+        DistroRulesVersion installedDistroRulesVersion = in.readParcelable(null);
+        return new RulesState(systemRulesVersion, distroFormatVersionSupported, operationInProgress,
+                distroStagedState, stagedDistroRulesVersion,
+                installedDistroStatus, installedDistroRulesVersion);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeString(mSystemRulesVersion);
+        out.writeParcelable(mDistroFormatVersionSupported, 0);
+        out.writeByte(mOperationInProgress ? BYTE_TRUE : BYTE_FALSE);
+        out.writeByte((byte) mStagedOperationType);
+        out.writeParcelable(mStagedDistroRulesVersion, 0);
+        out.writeByte((byte) mDistroStatus);
+        out.writeParcelable(mInstalledDistroRulesVersion, 0);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        RulesState that = (RulesState) o;
+
+        if (mOperationInProgress != that.mOperationInProgress) {
+            return false;
+        }
+        if (mStagedOperationType != that.mStagedOperationType) {
+            return false;
+        }
+        if (mDistroStatus != that.mDistroStatus) {
+            return false;
+        }
+        if (!mSystemRulesVersion.equals(that.mSystemRulesVersion)) {
+            return false;
+        }
+        if (!mDistroFormatVersionSupported.equals(that.mDistroFormatVersionSupported)) {
+            return false;
+        }
+        if (mStagedDistroRulesVersion != null ? !mStagedDistroRulesVersion
+                .equals(that.mStagedDistroRulesVersion) : that.mStagedDistroRulesVersion != null) {
+            return false;
+        }
+        return mInstalledDistroRulesVersion != null ? mInstalledDistroRulesVersion
+                .equals(that.mInstalledDistroRulesVersion)
+                : that.mInstalledDistroRulesVersion == null;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = mSystemRulesVersion.hashCode();
+        result = 31 * result + mDistroFormatVersionSupported.hashCode();
+        result = 31 * result + (mOperationInProgress ? 1 : 0);
+        result = 31 * result + mStagedOperationType;
+        result = 31 * result + (mStagedDistroRulesVersion != null ? mStagedDistroRulesVersion
+                .hashCode()
+                : 0);
+        result = 31 * result + mDistroStatus;
+        result = 31 * result + (mInstalledDistroRulesVersion != null ? mInstalledDistroRulesVersion
+                .hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "RulesState{"
+                + "mSystemRulesVersion='" + mSystemRulesVersion + '\''
+                + ", mDistroFormatVersionSupported=" + mDistroFormatVersionSupported
+                + ", mOperationInProgress=" + mOperationInProgress
+                + ", mStagedOperationType=" + mStagedOperationType
+                + ", mStagedDistroRulesVersion=" + mStagedDistroRulesVersion
+                + ", mDistroStatus=" + mDistroStatus
+                + ", mInstalledDistroRulesVersion=" + mInstalledDistroRulesVersion
+                + '}';
+    }
+
+    private static int validateStagedOperation(int stagedOperationType) {
+        if (stagedOperationType < STAGED_OPERATION_UNKNOWN
+                || stagedOperationType > STAGED_OPERATION_INSTALL) {
+            throw new IllegalArgumentException("Unknown operation type=" + stagedOperationType);
+        }
+        return stagedOperationType;
+    }
+
+    private static int validateDistroStatus(int distroStatus) {
+        if (distroStatus < DISTRO_STATUS_UNKNOWN || distroStatus > DISTRO_STATUS_INSTALLED) {
+            throw new IllegalArgumentException("Unknown distro status=" + distroStatus);
+        }
+        return distroStatus;
+    }
+}
diff --git a/core/java/android/app/timezone/RulesUpdaterContract.java b/core/java/android/app/timezone/RulesUpdaterContract.java
new file mode 100644
index 0000000..4e77818
--- /dev/null
+++ b/core/java/android/app/timezone/RulesUpdaterContract.java
@@ -0,0 +1,87 @@
+/*
+ * 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 android.app.timezone;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.ParcelFileDescriptor;
+
+/**
+ * Constants related to the contract between the Android system and the privileged time zone updater
+ * application.
+ *
+ * @hide
+ */
+// TODO(nfuller): Expose necessary APIs for OEMs with @SystemApi. http://b/31008728
+public final class RulesUpdaterContract {
+
+    /**
+     * The system permission possessed by the Android system that allows it to trigger time zone
+     * update checks. The updater should be configured to require this permission when registering
+     * for {@link #ACTION_TRIGGER_RULES_UPDATE_CHECK} intents.
+     */
+    public static final String TRIGGER_TIME_ZONE_RULES_CHECK_PERMISSION =
+            android.Manifest.permission.TRIGGER_TIME_ZONE_RULES_CHECK;
+
+    /**
+     * The system permission possessed by the time zone rules updater app that allows it to update
+     * device time zone rules. The Android system requires this permission for calls made to
+     * {@link RulesManager}.
+     */
+    public static final String UPDATE_TIME_ZONE_RULES_PERMISSION =
+            android.Manifest.permission.UPDATE_TIME_ZONE_RULES;
+
+    /**
+     * The action of the intent that the Android system will broadcast. The intent will be targeted
+     * at the configured updater application's package meaning the term "broadcast" only loosely
+     * applies.
+     */
+    public static final String ACTION_TRIGGER_RULES_UPDATE_CHECK =
+            "android.intent.action.timezone.TRIGGER_RULES_UPDATE_CHECK";
+
+    /**
+     * The extra containing the {@code byte[]} that should be passed to
+     * {@link RulesManager#requestInstall(ParcelFileDescriptor, byte[], Callback)},
+     * {@link RulesManager#requestUninstall(byte[], Callback)} and
+     * {@link RulesManager#requestNothing(byte[], boolean)} methods when the
+     * {@link #ACTION_TRIGGER_RULES_UPDATE_CHECK} intent has been processed.
+     */
+    public static final String EXTRA_CHECK_TOKEN =
+            "android.intent.extra.timezone.CHECK_TOKEN";
+
+    /**
+     * Creates an intent that would trigger a time zone rules update check.
+     */
+    public static Intent createUpdaterIntent(String updaterPackageName) {
+        Intent intent = new Intent(RulesUpdaterContract.ACTION_TRIGGER_RULES_UPDATE_CHECK);
+        intent.setPackage(updaterPackageName);
+        intent.setFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
+        return intent;
+    }
+
+    /**
+     * Broadcasts an {@link #ACTION_TRIGGER_RULES_UPDATE_CHECK} intent with the
+     * {@link #EXTRA_CHECK_TOKEN} that triggers an update check, including the required receiver
+     * permission.
+     */
+    public static void sendBroadcast(Context context, String updaterAppPackageName,
+            byte[] checkTokenBytes) {
+        Intent intent = createUpdaterIntent(updaterAppPackageName);
+        intent.putExtra(EXTRA_CHECK_TOKEN, checkTokenBytes);
+        context.sendBroadcast(intent, RulesUpdaterContract.UPDATE_TIME_ZONE_RULES_PERMISSION);
+    }
+}
diff --git a/core/java/android/app/timezone/Utils.java b/core/java/android/app/timezone/Utils.java
new file mode 100644
index 0000000..8dd3fb7
--- /dev/null
+++ b/core/java/android/app/timezone/Utils.java
@@ -0,0 +1,67 @@
+/*
+ * 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 android.app.timezone;
+
+/**
+ * Shared code for android.app.timezone classes.
+ */
+final class Utils {
+    private Utils() {}
+
+    static int validateVersion(String type, int version) {
+        if (version < 0 || version > 999) {
+            throw new IllegalArgumentException("Invalid " + type + " version=" + version);
+        }
+        return version;
+    }
+
+    static String validateRulesVersion(String type, String rulesVersion) {
+        validateNotNull(type, rulesVersion);
+
+        if (rulesVersion.isEmpty()) {
+            throw new IllegalArgumentException(type + " must not be empty");
+        }
+        return rulesVersion;
+    }
+
+    /** Validates that {@code object} is not null. Always returns {@code object}. */
+    static <T> T validateNotNull(String type, T object) {
+        if (object == null) {
+            throw new NullPointerException(type + " == null");
+        }
+        return object;
+    }
+
+    /**
+     * If {@code requireNotNull} is {@code true} calls {@link #validateNotNull(String, Object)},
+     * and {@link #validateNull(String, Object)} otherwise. Returns {@code object}.
+     */
+    static <T> T validateConditionalNull(boolean requireNotNull, String type, T object) {
+        if (requireNotNull) {
+            return validateNotNull(type, object);
+        } else {
+            return validateNull(type, object);
+        }
+    }
+
+    /** Validates that {@code object} is null. Always returns null. */
+    static <T> T validateNull(String type, T object) {
+        if (object != null) {
+            throw new IllegalArgumentException(type + " != null");
+        }
+        return null;
+    }
+}
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 3bbbe1c..b2f2cb7 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -2656,6 +2656,7 @@
             SENSOR_SERVICE,
             STORAGE_SERVICE,
             WALLPAPER_SERVICE,
+            TIME_ZONE_RULES_MANAGER_SERVICE,
             VIBRATOR_SERVICE,
             //@hide: STATUS_BAR_SERVICE,
             CONNECTIVITY_SERVICE,
@@ -3666,6 +3667,15 @@
     public static final String GATEKEEPER_SERVICE = "android.service.gatekeeper.IGateKeeperService";
 
     /**
+     * Use with {@link #getSystemService} to retrieve an
+     * {@link android.app.timezone.ITimeZoneRulesManager}.
+     * @hide
+     *
+     * @see #getSystemService
+     */
+    public static final String TIME_ZONE_RULES_MANAGER_SERVICE = "timezone";
+
+    /**
      * Determine whether the given permission is allowed for a particular
      * process and user ID running in the system.
      *
diff --git a/core/java/android/provider/TimeZoneRulesDataContract.java b/core/java/android/provider/TimeZoneRulesDataContract.java
new file mode 100644
index 0000000..19e914b
--- /dev/null
+++ b/core/java/android/provider/TimeZoneRulesDataContract.java
@@ -0,0 +1,114 @@
+/*
+ * 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 android.provider;
+
+import android.net.Uri;
+
+/**
+ * A set of constants for implementing a time zone data content provider, which is used by the time
+ * zone updater application.
+ *
+ * @hide
+ */
+// TODO(nfuller): Expose necessary APIs for OEMs with @SystemApi. http://b/31008728
+public final class TimeZoneRulesDataContract {
+
+    private TimeZoneRulesDataContract() {}
+
+    /**
+     * The authority that <em>must</em> be used for the time zone data content provider.
+     * To be accepted by the time zone updater application it <em>must</em> be exposed by the
+     * package specified in the config_timeZoneRulesDataPackage config value.
+     */
+    public static final String AUTHORITY = "com.android.timezone";
+
+    /** A content:// style uri to the authority for the time zone data content provider */
+    private static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
+
+    /**
+     * The content:// style URI for determining what type of update is available.
+     *
+     * <p>The URI can be queried using
+     * {@link android.content.ContentProvider#query(Uri, String[], String, String[], String)};
+     * the result will be a cursor with a single row. If the {@link #COLUMN_OPERATION}
+     * column is {@link #OPERATION_INSTALL} then see {@link #DATA_URI} for how to obtain the
+     * binary data.
+     */
+    public static final Uri OPERATION_URI = Uri.withAppendedPath(AUTHORITY_URI, "operation");
+
+    /**
+     * The {@code String} column of the {@link #OPERATION_URI} that provides an int specifying the
+     * type of operation to perform. See {@link #OPERATION_NO_OP}, {@link #OPERATION_UNINSTALL} and
+     * {@link #OPERATION_INSTALL}.
+     */
+    public static final String COLUMN_OPERATION = "operation";
+
+    /**
+     * An operation type used when the time zone rules on device should be left as they are.
+     * This is not expected to be used in normal operation but a safe result in the event of an
+     * error that cannot be recovered from.
+     */
+    public static final String OPERATION_NO_OP = "NOOP";
+
+    /**
+     * An operation type used when the current time zone rules on device should be uninstalled,
+     * returning to the values held in the system partition.
+     */
+    public static final String OPERATION_UNINSTALL = "UNINSTALL";
+
+    /**
+     * An operation type used when the current time zone rules on device should be replaced by
+     * a new set obtained via the {@link android.content.ContentProvider#openFile(Uri, String)}
+     * method.
+     */
+    public static final String OPERATION_INSTALL = "INSTALL";
+
+    /**
+     * The {@code nullable int} column of the {@link #OPERATION_URI} that describes the major
+     * version of the distro to be installed.
+     * Only non-null if {@link #COLUMN_OPERATION} contains {@link #OPERATION_INSTALL}.
+     */
+    public static final String COLUMN_DISTRO_MAJOR_VERSION = "distro_major_version";
+
+    /**
+     * The {@code nullable int} column of the {@link #OPERATION_URI} that describes the minor
+     * version of the distro to be installed.
+     * Only non-null if {@link #COLUMN_OPERATION} contains {@link #OPERATION_INSTALL}.
+     */
+    public static final String COLUMN_DISTRO_MINOR_VERSION = "distro_minor_version";
+
+    /**
+     * The {@code nullable String} column of the {@link #OPERATION_URI} that describes the IANA
+     * rules version of the distro to be installed.
+     * Only non-null if {@link #COLUMN_OPERATION} contains {@link #OPERATION_INSTALL}.
+     */
+    public static final String COLUMN_RULES_VERSION = "rules_version";
+
+    /**
+     * The {@code nullable int} column of the {@link #OPERATION_URI} that describes the revision
+     * number of the distro to be installed.
+     * Only non-null if {@link #COLUMN_OPERATION} contains {@link #OPERATION_INSTALL}.
+     */
+    public static final String COLUMN_REVISION = "revision";
+
+    /**
+     * The content:// style URI for obtaining time zone bundle data.
+     *
+     * <p>Use {@link android.content.ContentProvider#openFile(Uri, String)} with "r" mode.
+     */
+    public static final Uri DATA_URI = Uri.withAppendedPath(AUTHORITY_URI, "data");
+}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 4927be3..c2f27a9 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2094,6 +2094,22 @@
     <permission android:name="android.permission.UPDATE_CONFIG"
         android:protectionLevel="signature|privileged" />
 
+    <!-- Allows a time zone rule updater application to request
+         the system installs / uninstalls timezone rules.
+         <p>An application requesting this permission is responsible for
+         verifying the source and integrity of the update before passing
+         it off to the installer components.
+         @hide -->
+    <permission android:name="android.permission.UPDATE_TIME_ZONE_RULES"
+        android:protectionLevel="signature|privileged" />
+
+    <!-- Must be required by a time zone rule updater application,
+         to ensure that only the system can trigger it.
+         @hide -->
+    <permission android:name="android.permission.TRIGGER_TIME_ZONE_RULES_CHECK"
+        android:protectionLevel="signature" />
+    <uses-permission android:name="android.permission.TRIGGER_TIME_ZONE_RULES_CHECK"/>
+
     <!-- Allows the system to reset throttling in shortcut manager.
          @hide -->
     <permission android:name="android.permission.RESET_SHORTCUT_MANAGER_THROTTLING"
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 589aa07..22a1a36 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1282,6 +1282,49 @@
     <!-- True if WallpaperService is enabled -->
     <bool name="config_enableWallpaperService">true</bool>
 
+    <!-- Enables the TimeZoneRuleManager service. This is the master switch for the updateable time
+         zone update mechanism. -->
+    <bool name="config_enableUpdateableTimeZoneRules">false</bool>
+
+    <!-- Enables APK-based time zone update triggering. Set this to false when updates are triggered
+         via external events and not by APK updates. For example, if an updater checks with a server
+         on a regular schedule.
+         [This is only used if config_enableUpdateableTimeZoneRules is true.] -->
+    <bool name="config_timeZoneRulesUpdateTrackingEnabled">false</bool>
+
+    <!-- The package of the time zone rules updater application. Expected to be the same
+         for all Android devices that support APK-based time zone rule updates.
+         A package-targeted android.intent.action.timezone.TRIGGER_RULES_UPDATE_CHECK intent
+         will be sent to the updater app if the system server detects an update to the updater or
+         data app packages.
+         The package referenced here must have the android.permission.UPDATE_TIME_ZONE_RULES
+         permission.
+         [This is only used if config_enableUpdateableTimeZoneRules and
+         config_timeZoneRulesUpdateTrackingEnabled are true.] -->
+    <string name="config_timeZoneRulesUpdaterPackage" translateable="false"></string>
+
+    <!-- The package of the time zone rules data application. Expected to be configured
+         by OEMs to reference their own priv-app APK package.
+         A package-targeted android.intent.action.timezone.TRIGGER_RULES_UPDATE_CHECK intent
+         will be sent to the updater app if the system server detects an update to the updater or
+         data app packages.
+         [This is only used if config_enableUpdateableTimeZoneRules and
+         config_timeZoneRulesUpdateTrackingEnabled are true.] -->
+    <string name="config_timeZoneRulesDataPackage" translateable="false"></string>
+
+    <!-- The allowed time in milliseconds between an update check intent being broadcast and the
+         response being considered overdue. Reliability triggers will not fire in this time.
+         [This is only used if config_enableUpdateableTimeZoneRules and
+         config_timeZoneRulesUpdateTrackingEnabled are true.] -->
+    <!-- 5 minutes -->
+    <integer name="config_timeZoneRulesCheckTimeMillisAllowed">300000</integer>
+
+    <!-- The number of times a time zone update check is allowed to fail before the system will stop
+         reacting to reliability triggers.
+         [This is only used if config_enableUpdateableTimeZoneRules and
+         config_timeZoneRulesUpdateTrackingEnabled are true.] -->
+    <integer name="config_timeZoneRulesCheckRetryCount">5</integer>
+
     <!-- Whether to enable network location overlay which allows network
          location provider to be replaced by an app at run-time. When disabled,
          only the config_networkLocationProviderPackageName package will be
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 99dc9b4..ea89cc1 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -275,6 +275,12 @@
   <java-symbol type="bool" name="split_action_bar_is_narrow" />
   <java-symbol type="bool" name="config_useVolumeKeySounds" />
   <java-symbol type="bool" name="config_enableWallpaperService" />
+  <java-symbol type="bool" name="config_enableUpdateableTimeZoneRules" />
+  <java-symbol type="bool" name="config_timeZoneRulesUpdateTrackingEnabled" />
+  <java-symbol type="string" name="config_timeZoneRulesUpdaterPackage" />
+  <java-symbol type="string" name="config_timeZoneRulesDataPackage" />
+  <java-symbol type="integer" name="config_timeZoneRulesCheckTimeMillisAllowed" />
+  <java-symbol type="integer" name="config_timeZoneRulesCheckRetryCount" />
   <java-symbol type="bool" name="config_sendAudioBecomingNoisy" />
   <java-symbol type="bool" name="config_enableScreenshotChord" />
   <java-symbol type="bool" name="config_bluetooth_default_profiles" />
diff --git a/core/tests/coretests/src/android/app/timezone/DistroFormatVersionTest.java b/core/tests/coretests/src/android/app/timezone/DistroFormatVersionTest.java
new file mode 100644
index 0000000..9bbcd3d
--- /dev/null
+++ b/core/tests/coretests/src/android/app/timezone/DistroFormatVersionTest.java
@@ -0,0 +1,83 @@
+/*
+ * 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 android.app.timezone;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Parcel;
+
+import org.junit.Test;
+
+/**
+ * Tests for {@link DistroFormatVersion}.
+ */
+// TODO(nfuller) Move to CTS once this class is part of the SystemApi. http://b/31008728
+public class DistroFormatVersionTest {
+
+    @Test
+    public void equalsAndHashCode() {
+        DistroFormatVersion one = new DistroFormatVersion(1, 2);
+        assertEqualsContract(one, one);
+
+        DistroFormatVersion two = new DistroFormatVersion(1, 2);
+        assertEqualsContract(one, two);
+
+        DistroFormatVersion three = new DistroFormatVersion(2, 1);
+        assertFalse(one.equals(three));
+    }
+
+    @Test
+    public void parcelable() {
+        DistroFormatVersion version = new DistroFormatVersion(2, 3);
+
+        Parcel parcel = Parcel.obtain();
+        version.writeToParcel(parcel, 0 /* flags */);
+        parcel.setDataPosition(0);
+
+        DistroFormatVersion newVersion = DistroFormatVersion.CREATOR.createFromParcel(parcel);
+
+        assertEquals(version, newVersion);
+    }
+
+    @Test
+    public void supportsVersion() {
+        DistroFormatVersion deviceVersion = new DistroFormatVersion(2, 2);
+        assertTrue(deviceVersion.supports(deviceVersion));
+
+        DistroFormatVersion sameVersion = new DistroFormatVersion(2, 2);
+        assertTrue(deviceVersion.supports(sameVersion));
+
+        // Minor versions are backwards compatible.
+        DistroFormatVersion sameMajorNewerMinor = new DistroFormatVersion(2, 3);
+        assertTrue(deviceVersion.supports(sameMajorNewerMinor));
+        DistroFormatVersion sameMajorOlderMinor = new DistroFormatVersion(2, 1);
+        assertFalse(deviceVersion.supports(sameMajorOlderMinor));
+
+        // Major versions are not backwards compatible.
+        DistroFormatVersion newerMajor = new DistroFormatVersion(1, 2);
+        assertFalse(deviceVersion.supports(newerMajor));
+        DistroFormatVersion olderMajor = new DistroFormatVersion(3, 2);
+        assertFalse(deviceVersion.supports(olderMajor));
+    }
+
+    private static void assertEqualsContract(DistroFormatVersion one, DistroFormatVersion two) {
+        assertEquals(one, two);
+        assertEquals(one.hashCode(), two.hashCode());
+    }
+}
diff --git a/core/tests/coretests/src/android/app/timezone/DistroRulesVersionTest.java b/core/tests/coretests/src/android/app/timezone/DistroRulesVersionTest.java
new file mode 100644
index 0000000..2fbc9a1
--- /dev/null
+++ b/core/tests/coretests/src/android/app/timezone/DistroRulesVersionTest.java
@@ -0,0 +1,83 @@
+/*
+ * 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 android.app.timezone;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Parcel;
+
+import org.junit.Test;
+
+/**
+ * Tests for {@link DistroRulesVersion}.
+ */
+// TODO(nfuller) Move to CTS once this class is part of the SystemApi. http://b/31008728
+public class DistroRulesVersionTest {
+
+    @Test
+    public void equalsAndHashCode() {
+        DistroRulesVersion one = new DistroRulesVersion("2016a", 2);
+        assertEqualsContract(one, one);
+
+        DistroRulesVersion two = new DistroRulesVersion("2016a", 2);
+        assertEqualsContract(one, two);
+
+        DistroRulesVersion three = new DistroRulesVersion("2016b", 1);
+        assertFalse(one.equals(three));
+    }
+
+    @Test
+    public void parcelable() {
+        DistroRulesVersion version = new DistroRulesVersion("2016a", 2);
+
+        Parcel parcel = Parcel.obtain();
+        version.writeToParcel(parcel, 0 /* flags */);
+        parcel.setDataPosition(0);
+
+        DistroRulesVersion newVersion = DistroRulesVersion.CREATOR.createFromParcel(parcel);
+
+        assertEquals(version, newVersion);
+    }
+
+    @Test
+    public void isOlderThan() {
+        DistroRulesVersion deviceVersion = new DistroRulesVersion("2016b", 2);
+        assertFalse(deviceVersion.isOlderThan(deviceVersion));
+
+        DistroRulesVersion sameVersion = new DistroRulesVersion("2016b", 2);
+        assertFalse(deviceVersion.isOlderThan(sameVersion));
+
+        DistroRulesVersion sameRulesNewerRevision = new DistroRulesVersion("2016b", 3);
+        assertTrue(deviceVersion.isOlderThan(sameRulesNewerRevision));
+
+        DistroRulesVersion sameRulesOlderRevision = new DistroRulesVersion("2016b", 1);
+        assertFalse(deviceVersion.isOlderThan(sameRulesOlderRevision));
+
+        DistroRulesVersion newerRules = new DistroRulesVersion("2016c", 2);
+        assertTrue(deviceVersion.isOlderThan(newerRules));
+
+        DistroRulesVersion olderRules = new DistroRulesVersion("2016a", 2);
+        assertFalse(deviceVersion.isOlderThan(olderRules));
+    }
+
+    private static void assertEqualsContract(DistroRulesVersion one, DistroRulesVersion two) {
+        assertEquals(one, two);
+        assertEquals(one.hashCode(), two.hashCode());
+    }
+}
diff --git a/core/tests/coretests/src/android/app/timezone/RulesStateTest.java b/core/tests/coretests/src/android/app/timezone/RulesStateTest.java
new file mode 100644
index 0000000..a9357c9
--- /dev/null
+++ b/core/tests/coretests/src/android/app/timezone/RulesStateTest.java
@@ -0,0 +1,187 @@
+/*
+ * 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 android.app.timezone;
+
+import static junit.framework.Assert.fail;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Parcel;
+
+import org.junit.Test;
+
+/**
+ * Tests for {@link RulesState}.
+ */
+// TODO(nfuller) Move to CTS once this class is part of the SystemApi. http://b/31008728
+public class RulesStateTest {
+
+    @Test
+    public void equalsAndHashCode() {
+        RulesState one = new RulesState(
+                "2016a", formatVersion(1, 2), false /* operationInProgress */,
+                RulesState.STAGED_OPERATION_INSTALL, rulesVersion("2016a", 3),
+                RulesState.DISTRO_STATUS_INSTALLED, rulesVersion("2016b", 2));
+        assertEqualsContract(one, one);
+
+        RulesState two = new RulesState(
+                "2016a", formatVersion(1, 2), false /* operationInProgress */,
+                RulesState.STAGED_OPERATION_INSTALL, rulesVersion("2016a", 3),
+                RulesState.DISTRO_STATUS_INSTALLED, rulesVersion("2016b", 2));
+        assertEqualsContract(one, two);
+
+        RulesState differentSystemRules = new RulesState(
+                "2016b", formatVersion(1, 2), false /* operationInProgress */,
+                RulesState.STAGED_OPERATION_INSTALL, rulesVersion("2016a", 3),
+                RulesState.DISTRO_STATUS_INSTALLED, rulesVersion("2016b", 2));
+        assertFalse(one.equals(differentSystemRules));
+
+        RulesState differentFormatVersion = new RulesState(
+                "2016a", formatVersion(1, 1), false /* operationInProgress */,
+                RulesState.STAGED_OPERATION_INSTALL, rulesVersion("2016a", 3),
+                RulesState.DISTRO_STATUS_INSTALLED, rulesVersion("2016b", 2));
+        assertFalse(one.equals(differentFormatVersion));
+
+        RulesState differentOperationInProgress = new RulesState(
+                "2016a", formatVersion(1, 1), true /* operationInProgress */,
+                RulesState.STAGED_OPERATION_UNKNOWN, null /* stagedDistroRulesVersion */,
+                RulesState.DISTRO_STATUS_UNKNOWN, null /* installedDistroRulesVersion */);
+        assertFalse(one.equals(differentOperationInProgress));
+
+        RulesState differentStagedOperation = new RulesState(
+                "2016a", formatVersion(1, 1), false /* operationInProgress */,
+                RulesState.STAGED_OPERATION_UNINSTALL, null /* stagedDistroRulesVersion */,
+                RulesState.DISTRO_STATUS_INSTALLED, rulesVersion("2016b", 2));
+        assertFalse(one.equals(differentStagedOperation));
+
+        RulesState differentStagedInstallVersion = new RulesState(
+                "2016a", formatVersion(1, 1), false /* operationInProgress */,
+                RulesState.STAGED_OPERATION_INSTALL, rulesVersion("2016a", 4),
+                RulesState.DISTRO_STATUS_INSTALLED, rulesVersion("2016b", 2));
+        assertFalse(one.equals(differentStagedInstallVersion));
+
+        RulesState differentInstalled = new RulesState(
+                "2016a", formatVersion(1, 1), false /* operationInProgress */,
+                RulesState.STAGED_OPERATION_INSTALL, rulesVersion("2016a", 3),
+                RulesState.DISTRO_STATUS_NONE, null /* installedDistroRulesVersion */);
+        assertFalse(one.equals(differentInstalled));
+
+        RulesState differentInstalledVersion = new RulesState(
+                "2016a", formatVersion(1, 1), false /* operationInProgress */,
+                RulesState.STAGED_OPERATION_INSTALL, rulesVersion("2016a", 3),
+                RulesState.DISTRO_STATUS_INSTALLED, rulesVersion("2016b", 3));
+        assertFalse(one.equals(differentInstalledVersion));
+    }
+
+    @Test
+    public void parcelable() {
+        RulesState rulesState1 = new RulesState(
+                "2016a", formatVersion(1, 1), false /* operationInProgress */,
+                RulesState.STAGED_OPERATION_INSTALL, rulesVersion("2016b", 2),
+                RulesState.DISTRO_STATUS_INSTALLED, rulesVersion("2016b", 3));
+        checkParcelableRoundTrip(rulesState1);
+
+        RulesState rulesStateWithNulls = new RulesState(
+                "2016a", formatVersion(1, 1), false /* operationInProgress */,
+                RulesState.STAGED_OPERATION_NONE, null /* stagedDistroRulesVersion */,
+                RulesState.DISTRO_STATUS_NONE, null /* installedDistroRulesVersion */);
+        checkParcelableRoundTrip(rulesStateWithNulls);
+
+        RulesState rulesStateWithUnknowns = new RulesState(
+                "2016a", formatVersion(1, 1), true /* operationInProgress */,
+                RulesState.STAGED_OPERATION_UNKNOWN, null /* stagedDistroRulesVersion */,
+                RulesState.DISTRO_STATUS_UNKNOWN, null /* installedDistroRulesVersion */);
+        checkParcelableRoundTrip(rulesStateWithNulls);
+    }
+
+    private static void checkParcelableRoundTrip(RulesState rulesState) {
+        Parcel parcel = Parcel.obtain();
+        rulesState.writeToParcel(parcel, 0 /* flags */);
+        parcel.setDataPosition(0);
+
+        RulesState newVersion = RulesState.CREATOR.createFromParcel(parcel);
+
+        assertEquals(rulesState, newVersion);
+    }
+
+    @Test
+    public void isSystemVersionOlderThan() {
+        RulesState rulesState = new RulesState(
+                "2016b", formatVersion(1, 1), false /* operationInProgress */,
+                RulesState.STAGED_OPERATION_NONE, null /* stagedDistroRulesVersion */,
+                RulesState.DISTRO_STATUS_INSTALLED, rulesVersion("2016b", 3));
+        assertFalse(rulesState.isSystemVersionOlderThan(rulesVersion("2016a", 1)));
+        assertFalse(rulesState.isSystemVersionOlderThan(rulesVersion("2016b", 1)));
+        assertTrue(rulesState.isSystemVersionOlderThan(rulesVersion("2016c", 1)));
+    }
+
+    @Test
+    public void isInstalledDistroOlderThan() {
+        RulesState operationInProgress = new RulesState(
+                "2016b", formatVersion(1, 1), true /* operationInProgress */,
+                RulesState.STAGED_OPERATION_UNKNOWN, null /* stagedDistroRulesVersion */,
+                RulesState.STAGED_OPERATION_UNKNOWN, null /* installedDistroRulesVersion */);
+        try {
+            operationInProgress.isInstalledDistroOlderThan(rulesVersion("2016b", 1));
+            fail();
+        } catch (IllegalStateException expected) {
+        }
+
+        RulesState nothingInstalled = new RulesState(
+                "2016b", formatVersion(1, 1), false /* operationInProgress */,
+                RulesState.STAGED_OPERATION_NONE, null /* stagedDistroRulesVersion */,
+                RulesState.DISTRO_STATUS_NONE, null /* installedDistroRulesVersion */);
+        try {
+            nothingInstalled.isInstalledDistroOlderThan(rulesVersion("2016b", 1));
+            fail();
+        } catch (IllegalStateException expected) {
+        }
+
+        DistroRulesVersion installedVersion = rulesVersion("2016b", 3);
+        RulesState rulesStateWithInstalledVersion = new RulesState(
+                "2016b", formatVersion(1, 1), false /* operationInProgress */,
+                RulesState.STAGED_OPERATION_NONE, null /* stagedDistroRulesVersion */,
+                RulesState.DISTRO_STATUS_INSTALLED, installedVersion);
+
+        DistroRulesVersion olderRules = rulesVersion("2016a", 1);
+        assertEquals(installedVersion.isOlderThan(olderRules),
+                rulesStateWithInstalledVersion.isInstalledDistroOlderThan(olderRules));
+
+        DistroRulesVersion sameRules = rulesVersion("2016b", 1);
+        assertEquals(installedVersion.isOlderThan(sameRules),
+                rulesStateWithInstalledVersion.isInstalledDistroOlderThan(sameRules));
+
+        DistroRulesVersion newerRules = rulesVersion("2016c", 1);
+        assertEquals(installedVersion.isOlderThan(newerRules),
+                rulesStateWithInstalledVersion.isInstalledDistroOlderThan(newerRules));
+    }
+
+    private static void assertEqualsContract(RulesState one, RulesState two) {
+        assertEquals(one, two);
+        assertEquals(one.hashCode(), two.hashCode());
+    }
+
+    private static DistroRulesVersion rulesVersion(String rulesVersion, int revision) {
+        return new DistroRulesVersion(rulesVersion, revision);
+    }
+
+    private static DistroFormatVersion formatVersion(int majorVersion, int minorVersion) {
+        return new DistroFormatVersion(majorVersion, minorVersion);
+    }
+}
diff --git a/core/tests/coretests/src/android/app/timezone/RulesUpdaterContractTest.java b/core/tests/coretests/src/android/app/timezone/RulesUpdaterContractTest.java
new file mode 100644
index 0000000..e7a839c
--- /dev/null
+++ b/core/tests/coretests/src/android/app/timezone/RulesUpdaterContractTest.java
@@ -0,0 +1,83 @@
+/*
+ * 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 android.app.timezone;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.hamcrest.MockitoHamcrest.argThat;
+
+import android.content.Context;
+import android.content.Intent;
+
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.junit.Test;
+
+/**
+ * Tests for {@link RulesUpdaterContract}.
+ */
+// TODO(nfuller) Move to CTS once this class is part of the SystemApi. http://b/31008728
+public class RulesUpdaterContractTest {
+
+    @Test
+    public void createUpdaterIntent() throws Exception {
+        String packageName = "foobar";
+        Intent intent = RulesUpdaterContract.createUpdaterIntent(packageName);
+
+        assertEquals(RulesUpdaterContract.ACTION_TRIGGER_RULES_UPDATE_CHECK, intent.getAction());
+        assertEquals(packageName, intent.getPackage());
+        assertEquals(Intent.FLAG_INCLUDE_STOPPED_PACKAGES, intent.getFlags());
+    }
+
+    @Test
+    public void sendBroadcast() throws Exception {
+        String packageName = "foobar";
+        byte[] tokenBytes = new byte[] { 1, 2, 3, 4, 5 };
+
+        Intent expectedIntent = RulesUpdaterContract.createUpdaterIntent(packageName);
+        expectedIntent.putExtra(RulesUpdaterContract.EXTRA_CHECK_TOKEN, tokenBytes);
+
+        Context mockContext = mock(Context.class);
+
+        RulesUpdaterContract.sendBroadcast(mockContext, packageName, tokenBytes);
+
+        verify(mockContext).sendBroadcast(
+                filterEquals(expectedIntent),
+                eq(RulesUpdaterContract.UPDATE_TIME_ZONE_RULES_PERMISSION));
+    }
+
+    /**
+     * Registers a mockito parameter matcher that uses {@link Intent#filterEquals(Intent)}. to
+     * check the parameter against the intent supplied.
+     */
+    private static Intent filterEquals(final Intent expected) {
+        final Matcher<Intent> m = new BaseMatcher<Intent>() {
+            @Override
+            public boolean matches(Object actual) {
+                return actual != null && expected.filterEquals((Intent) actual);
+            }
+            @Override
+            public void describeTo(Description description) {
+                description.appendText(expected.toString());
+            }
+        };
+        return argThat(m);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EmergencyCryptkeeperText.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/EmergencyCryptkeeperText.java
index a2d1baf..c726189 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EmergencyCryptkeeperText.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/EmergencyCryptkeeperText.java
@@ -47,6 +47,11 @@
         public void onPhoneStateChanged(int phoneState) {
             update();
         }
+
+        @Override
+        public void onRefreshCarrierInfo() {
+            update();
+        }
     };
     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
         @Override
diff --git a/rs/java/android/renderscript/ScriptIntrinsicLUT.java b/rs/java/android/renderscript/ScriptIntrinsicLUT.java
index 69ff64a..e90462d 100644
--- a/rs/java/android/renderscript/ScriptIntrinsicLUT.java
+++ b/rs/java/android/renderscript/ScriptIntrinsicLUT.java
@@ -56,6 +56,10 @@
 
     }
 
+    public void destroy() {
+        mTables.destroy();
+        super.destroy();
+    }
 
     private void validate(int index, int value) {
         if (index < 0 || index > 255) {
diff --git a/services/core/java/com/android/server/pm/PackageDexOptimizer.java b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
index 8e0997b..ddb3639 100644
--- a/services/core/java/com/android/server/pm/PackageDexOptimizer.java
+++ b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
@@ -52,7 +52,8 @@
 import static com.android.server.pm.InstructionSets.getAppDexInstructionSets;
 import static com.android.server.pm.InstructionSets.getDexCodeInstructionSets;
 
-import static com.android.server.pm.PackageManagerServiceCompilerMapping.getNonProfileGuidedCompilerFilter;
+import static dalvik.system.DexFile.getNonProfileGuidedCompilerFilter;
+import static dalvik.system.DexFile.getSafeModeCompilerFilter;
 import static dalvik.system.DexFile.isProfileGuidedCompilerFilter;
 
 /**
@@ -359,13 +360,7 @@
         int flags = info.flags;
         boolean vmSafeMode = (flags & ApplicationInfo.FLAG_VM_SAFE_MODE) != 0;
         if (vmSafeMode) {
-            // For the compilation, it doesn't really matter what we return here because installd
-            // will replace the filter with 'quicken' anyway.
-            // However, we return a non profile guided filter so that we simplify the logic of
-            // merging profiles.
-            // TODO(calin): safe mode path could be simplified if we pass 'quicken' from
-            //              here rather than letting installd decide on the filter.
-            return getNonProfileGuidedCompilerFilter(targetCompilerFilter);
+            return getSafeModeCompilerFilter(targetCompilerFilter);
         }
 
         if (isProfileGuidedCompilerFilter(targetCompilerFilter) && isUsedByOtherApps) {
@@ -549,9 +544,13 @@
 
         @Override
         protected int adjustDexoptNeeded(int dexoptNeeded) {
-            // Ensure compilation, no matter the current state.
-            // TODO: The return value is wrong when patchoat is needed.
-            return DexFile.DEX2OAT_FROM_SCRATCH;
+            if (dexoptNeeded == DexFile.NO_DEXOPT_NEEDED) {
+                // Ensure compilation by pretending a compiler filter change on the
+                // apk/odex location (the reason for the '-'. A positive value means
+                // the 'oat' location).
+                return -DexFile.DEX2OAT_FOR_FILTER;
+            }
+            return dexoptNeeded;
         }
 
         @Override
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 3196b09..2391abd 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -92,11 +92,12 @@
 import static com.android.server.pm.InstructionSets.getPrimaryInstructionSet;
 import static com.android.server.pm.PackageManagerServiceCompilerMapping.getCompilerFilterForReason;
 import static com.android.server.pm.PackageManagerServiceCompilerMapping.getFullCompilerFilter;
-import static com.android.server.pm.PackageManagerServiceCompilerMapping.getNonProfileGuidedCompilerFilter;
 import static com.android.server.pm.PermissionsState.PERMISSION_OPERATION_FAILURE;
 import static com.android.server.pm.PermissionsState.PERMISSION_OPERATION_SUCCESS;
 import static com.android.server.pm.PermissionsState.PERMISSION_OPERATION_SUCCESS_GIDS_CHANGED;
 
+import static dalvik.system.DexFile.getNonProfileGuidedCompilerFilter;
+
 import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceCompilerMapping.java b/services/core/java/com/android/server/pm/PackageManagerServiceCompilerMapping.java
index 0634dac..f56534d 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceCompilerMapping.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceCompilerMapping.java
@@ -123,11 +123,4 @@
 
         return value;
     }
-
-    /**
-     * Return the non-profile-guided filter corresponding to the given filter.
-     */
-    public static String getNonProfileGuidedCompilerFilter(String filter) {
-        return DexFile.getNonProfileGuidedCompilerFilter(filter);
-    }
 }
diff --git a/services/core/java/com/android/server/timezone/CheckToken.java b/services/core/java/com/android/server/timezone/CheckToken.java
new file mode 100644
index 0000000..5128360
--- /dev/null
+++ b/services/core/java/com/android/server/timezone/CheckToken.java
@@ -0,0 +1,98 @@
+/*
+ * 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 java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * A deserialized version of the byte[] sent to the time zone update application to identify a
+ * triggered time zone update check. It encodes the optimistic lock ID used to detect
+ * concurrent checks and the minimal package versions that will have been checked.
+ */
+final class CheckToken {
+
+    final int mOptimisticLockId;
+    final PackageVersions mPackageVersions;
+
+    CheckToken(int optimisticLockId, PackageVersions packageVersions) {
+        this.mOptimisticLockId = optimisticLockId;
+
+        if (packageVersions == null) {
+            throw new NullPointerException("packageVersions == null");
+        }
+        this.mPackageVersions = packageVersions;
+    }
+
+    byte[] toByteArray() {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream(12 /* (3 * sizeof(int)) */);
+        try (DataOutputStream dos = new DataOutputStream(baos)) {
+            dos.writeInt(mOptimisticLockId);
+            dos.writeInt(mPackageVersions.mUpdateAppVersion);
+            dos.writeInt(mPackageVersions.mDataAppVersion);
+        } catch (IOException e) {
+            throw new RuntimeException("Unable to write into a ByteArrayOutputStream", e);
+        }
+        return baos.toByteArray();
+    }
+
+    static CheckToken fromByteArray(byte[] tokenBytes) throws IOException {
+        ByteArrayInputStream bais = new ByteArrayInputStream(tokenBytes);
+        try (DataInputStream dis = new DataInputStream(bais)) {
+            int versionId = dis.readInt();
+            int updateAppVersion = dis.readInt();
+            int dataAppVersion = dis.readInt();
+            return new CheckToken(versionId, new PackageVersions(updateAppVersion, dataAppVersion));
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        CheckToken checkToken = (CheckToken) o;
+
+        if (mOptimisticLockId != checkToken.mOptimisticLockId) {
+            return false;
+        }
+        return mPackageVersions.equals(checkToken.mPackageVersions);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = mOptimisticLockId;
+        result = 31 * result + mPackageVersions.hashCode();
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "Token{" +
+                "mOptimisticLockId=" + mOptimisticLockId +
+                ", mPackageVersions=" + mPackageVersions +
+                '}';
+    }
+}
diff --git a/services/core/java/com/android/server/timezone/ClockHelper.java b/services/core/java/com/android/server/timezone/ClockHelper.java
new file mode 100644
index 0000000..353728a
--- /dev/null
+++ b/services/core/java/com/android/server/timezone/ClockHelper.java
@@ -0,0 +1,25 @@
+/*
+ * 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;
+
+/**
+ * An easy-to-mock interface for obtaining a monotonically increasing time value in milliseconds.
+ */
+interface ClockHelper {
+
+    long currentTimestamp();
+}
diff --git a/services/core/java/com/android/server/timezone/ConfigHelper.java b/services/core/java/com/android/server/timezone/ConfigHelper.java
new file mode 100644
index 0000000..f9984fa
--- /dev/null
+++ b/services/core/java/com/android/server/timezone/ConfigHelper.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+/**
+ * An easy-to-mock interface around device config for use by {@link PackageTracker}; it is not
+ * possible to test various states with the real one because config is fixed in the system image.
+ */
+interface ConfigHelper {
+
+    boolean isTrackingEnabled();
+
+    String getUpdateAppPackageName();
+
+    String getDataAppPackageName();
+
+    int getCheckTimeAllowedMillis();
+
+    int getFailedCheckRetryCount();
+}
diff --git a/services/core/java/com/android/server/timezone/FileDescriptorHelper.java b/services/core/java/com/android/server/timezone/FileDescriptorHelper.java
new file mode 100644
index 0000000..c3b1101
--- /dev/null
+++ b/services/core/java/com/android/server/timezone/FileDescriptorHelper.java
@@ -0,0 +1,30 @@
+/*
+ * 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 android.os.ParcelFileDescriptor;
+
+import java.io.IOException;
+
+/**
+ * An easy-to-mock interface around use of {@link ParcelFileDescriptor} for use by
+ * {@link RulesManagerService}.
+ */
+interface FileDescriptorHelper {
+
+    byte[] readFully(ParcelFileDescriptor parcelFileDescriptor) throws IOException;
+}
diff --git a/services/core/java/com/android/server/timezone/IntentHelper.java b/services/core/java/com/android/server/timezone/IntentHelper.java
new file mode 100644
index 0000000..0cb9065
--- /dev/null
+++ b/services/core/java/com/android/server/timezone/IntentHelper.java
@@ -0,0 +1,37 @@
+/*
+ * 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;
+
+/**
+ * An easy-to-mock interface around intent sending / receiving for use by {@link PackageTracker};
+ * it is not possible to test various cases with the real one because of the need to simulate
+ * receiving and broadcasting intents.
+ */
+interface IntentHelper {
+
+    void initialize(String updateAppPackageName, String dataAppPackageName, Listener listener);
+
+    void sendTriggerUpdateCheck(CheckToken checkToken);
+
+    void enableReliabilityTriggering();
+
+    void disableReliabilityTriggering();
+
+    interface Listener {
+        void triggerUpdateIfNeeded(boolean packageUpdated);
+    }
+}
diff --git a/services/core/java/com/android/server/timezone/IntentHelperImpl.java b/services/core/java/com/android/server/timezone/IntentHelperImpl.java
new file mode 100644
index 0000000..3ffbb2d
--- /dev/null
+++ b/services/core/java/com/android/server/timezone/IntentHelperImpl.java
@@ -0,0 +1,116 @@
+/*
+ * 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 android.app.timezone.RulesUpdaterContract;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.PatternMatcher;
+import android.util.Slog;
+
+import java.util.regex.Pattern;
+
+/**
+ * The bona fide implementation of {@link IntentHelper}.
+ */
+final class IntentHelperImpl implements IntentHelper {
+
+    private final static String TAG = "timezone.IntentHelperImpl";
+
+    private final Context mContext;
+    private String mUpdaterAppPackageName;
+
+    private boolean mReliabilityReceiverEnabled;
+    private Receiver mReliabilityReceiver;
+
+    IntentHelperImpl(Context context) {
+        mContext = context;
+    }
+
+    @Override
+    public void initialize(
+            String updaterAppPackageName, String dataAppPackageName, Listener listener) {
+        mUpdaterAppPackageName = updaterAppPackageName;
+
+        // Register for events of interest.
+
+        // The intent filter that triggers when package update events happen that indicate there may
+        // be work to do.
+        IntentFilter packageIntentFilter = new IntentFilter();
+        // Either of these mean a downgrade?
+        packageIntentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+        packageIntentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED);
+        packageIntentFilter.addDataScheme("package");
+        packageIntentFilter.addDataSchemeSpecificPart(
+                updaterAppPackageName, PatternMatcher.PATTERN_LITERAL);
+        packageIntentFilter.addDataSchemeSpecificPart(
+                dataAppPackageName, PatternMatcher.PATTERN_LITERAL);
+        Receiver packageUpdateReceiver = new Receiver(listener, true /* packageUpdated */);
+        mContext.registerReceiver(packageUpdateReceiver, packageIntentFilter);
+
+        // TODO(nfuller): Add more exotic intents as needed. e.g.
+        // packageIntentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        // Also, disabled...?
+        mReliabilityReceiver = new Receiver(listener, false /* packageUpdated */);
+    }
+
+    /** Sends an intent to trigger an update check. */
+    @Override
+    public void sendTriggerUpdateCheck(CheckToken checkToken) {
+        RulesUpdaterContract.sendBroadcast(
+                mContext, mUpdaterAppPackageName, checkToken.toByteArray());
+    }
+
+    @Override
+    public synchronized void enableReliabilityTriggering() {
+        if (!mReliabilityReceiverEnabled) {
+            // The intent filter that exists to make updates reliable in the event of failures /
+            // reboots.
+            IntentFilter reliabilityIntentFilter = new IntentFilter();
+            reliabilityIntentFilter.addAction(Intent.ACTION_IDLE_MAINTENANCE_START);
+            mContext.registerReceiver(mReliabilityReceiver, reliabilityIntentFilter);
+            mReliabilityReceiverEnabled = true;
+        }
+    }
+
+    @Override
+    public synchronized void disableReliabilityTriggering() {
+        if (mReliabilityReceiverEnabled) {
+            mContext.unregisterReceiver(mReliabilityReceiver);
+            mReliabilityReceiverEnabled = false;
+        }
+    }
+
+    private static class Receiver extends BroadcastReceiver {
+        private final Listener mListener;
+        private final boolean mPackageUpdated;
+
+        private Receiver(Listener listener, boolean packageUpdated) {
+            mListener = listener;
+            mPackageUpdated = packageUpdated;
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            Slog.d(TAG, "Received intent: " + intent.toString());
+            mListener.triggerUpdateIfNeeded(mPackageUpdated);
+        }
+    }
+
+}
diff --git a/services/core/java/com/android/server/timezone/PackageManagerHelper.java b/services/core/java/com/android/server/timezone/PackageManagerHelper.java
new file mode 100644
index 0000000..804941a
--- /dev/null
+++ b/services/core/java/com/android/server/timezone/PackageManagerHelper.java
@@ -0,0 +1,41 @@
+/*
+ * 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 android.content.Intent;
+import android.content.pm.PackageManager;
+
+/**
+ * An easy-to-mock facade around PackageManager for use by {@link PackageTracker}; it is not
+ * possible to test various cases with the real one because of the need to simulate package versions
+ * and manifest configurations.
+ */
+interface PackageManagerHelper {
+
+    int getInstalledPackageVersion(String packageName)
+            throws PackageManager.NameNotFoundException;
+
+    boolean isPrivilegedApp(String packageName) throws PackageManager.NameNotFoundException;
+
+    boolean usesPermission(String packageName, String requiredPermissionName)
+                    throws PackageManager.NameNotFoundException;
+
+    boolean contentProviderRegistered(String authority, String requiredPackageName);
+
+    boolean receiverRegistered(Intent intent, String requiredPermissionName)
+                            throws PackageManager.NameNotFoundException;
+}
diff --git a/services/core/java/com/android/server/timezone/PackageStatus.java b/services/core/java/com/android/server/timezone/PackageStatus.java
new file mode 100644
index 0000000..63790961
--- /dev/null
+++ b/services/core/java/com/android/server/timezone/PackageStatus.java
@@ -0,0 +1,89 @@
+/*
+ * 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 android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Information about the status of the time zone update / data packages that are persisted by the
+ * Android system.
+ */
+final class PackageStatus {
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({ CHECK_STARTED, CHECK_COMPLETED_SUCCESS, CHECK_COMPLETED_FAILURE })
+    @interface CheckStatus {}
+
+    /** A time zone update check has been started but not yet completed. */
+    static final int CHECK_STARTED = 1;
+    /** A time zone update check has been completed and succeeded. */
+    static final int CHECK_COMPLETED_SUCCESS = 2;
+    /** A time zone update check has been completed and failed. */
+    static final int CHECK_COMPLETED_FAILURE = 3;
+
+    @CheckStatus
+    final int mCheckStatus;
+
+    // Non-null
+    final PackageVersions mVersions;
+
+    PackageStatus(@CheckStatus int checkStatus, PackageVersions versions) {
+        this.mCheckStatus = checkStatus;
+        if (checkStatus < 1 || checkStatus > 3) {
+            throw new IllegalArgumentException("Unknown checkStatus " + checkStatus);
+        }
+        if (versions == null) {
+            throw new NullPointerException("versions == null");
+        }
+        this.mVersions = versions;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        PackageStatus that = (PackageStatus) o;
+
+        if (mCheckStatus != that.mCheckStatus) {
+            return false;
+        }
+        return mVersions.equals(that.mVersions);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = mCheckStatus;
+        result = 31 * result + mVersions.hashCode();
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "PackageStatus{" +
+                "mCheckStatus=" + mCheckStatus +
+                ", mVersions=" + mVersions +
+                '}';
+    }
+}
diff --git a/services/core/java/com/android/server/timezone/PackageStatusStorage.java b/services/core/java/com/android/server/timezone/PackageStatusStorage.java
new file mode 100644
index 0000000..31f0e31
--- /dev/null
+++ b/services/core/java/com/android/server/timezone/PackageStatusStorage.java
@@ -0,0 +1,336 @@
+/*
+ * 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 android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Slog;
+
+import java.io.File;
+
+import static com.android.server.timezone.PackageStatus.CHECK_COMPLETED_FAILURE;
+import static com.android.server.timezone.PackageStatus.CHECK_COMPLETED_SUCCESS;
+import static com.android.server.timezone.PackageStatus.CHECK_STARTED;
+
+/**
+ * Storage logic for accessing/mutating the Android system's persistent state related to time zone
+ * update checking. There is expected to be a single instance and all methods synchronized on
+ * {@code this} for thread safety.
+ */
+final class PackageStatusStorage {
+
+    private static final String TAG = "timezone.PackageStatusStorage";
+
+    private static final String DATABASE_NAME = "timezonepackagestatus.db";
+    private static final int DATABASE_VERSION = 1;
+
+    /** The table name. It will have a single row with _id == {@link #SINGLETON_ID} */
+    private static final String TABLE = "status";
+    private static final String COLUMN_ID = "_id";
+
+    /**
+     * Column that stores a monotonically increasing lock ID, used to detect concurrent update
+     * issues without on-line locks. Incremented on every write.
+     */
+    private static final String COLUMN_OPTIMISTIC_LOCK_ID = "optimistic_lock_id";
+
+    /**
+     * Column that stores the current "check status" of the time zone update application packages.
+     */
+    private static final String COLUMN_CHECK_STATUS = "check_status";
+
+    /**
+     * Column that stores the version of the time zone rules update application being checked / last
+     * checked.
+     */
+    private static final String COLUMN_UPDATE_APP_VERSION = "update_app_package_version";
+
+    /**
+     * Column that stores the version of the time zone rules data application being checked / last
+     * checked.
+     */
+    private static final String COLUMN_DATA_APP_VERSION = "data_app_package_version";
+
+    /**
+     * The ID of the one row.
+     */
+    private static final int SINGLETON_ID = 1;
+
+    private static final int UNKNOWN_PACKAGE_VERSION = -1;
+
+    private final DatabaseHelper mDatabaseHelper;
+
+    PackageStatusStorage(Context context) {
+        mDatabaseHelper = new DatabaseHelper(context);
+    }
+
+    void deleteDatabaseForTests() {
+        SQLiteDatabase.deleteDatabase(mDatabaseHelper.getDatabaseFile());
+    }
+
+    /**
+     * Obtain the current check status of the application packages. Returns {@code null} the first
+     * time it is called, or after {@link #resetCheckState()}.
+     */
+    PackageStatus getPackageStatus() {
+        synchronized (this) {
+            try {
+                return getPackageStatusInternal();
+            } catch (IllegalArgumentException e) {
+                // This means that data exists in the table but it was bad.
+                Slog.e(TAG, "Package status invalid, resetting and retrying", e);
+
+                // Reset the storage so it is in a good state again.
+                mDatabaseHelper.recoverFromBadData();
+                return getPackageStatusInternal();
+            }
+        }
+    }
+
+    private PackageStatus getPackageStatusInternal() {
+        String[] columns = {
+                COLUMN_CHECK_STATUS, COLUMN_UPDATE_APP_VERSION, COLUMN_DATA_APP_VERSION
+        };
+        Cursor cursor = mDatabaseHelper.getReadableDatabase()
+                .query(TABLE, columns, COLUMN_ID + " = ?",
+                        new String[] { Integer.toString(SINGLETON_ID) },
+                        null /* groupBy */, null /* having */, null /* orderBy */);
+        if (cursor.getCount() != 1) {
+            Slog.e(TAG, "Unable to find package status from package status row. Rows returned: "
+                    + cursor.getCount());
+            return null;
+        }
+        cursor.moveToFirst();
+
+        // Determine check status.
+        if (cursor.isNull(0)) {
+            // This is normal the first time getPackageStatus() is called, or after
+            // resetCheckState().
+            return null;
+        }
+        int checkStatus = cursor.getInt(0);
+
+        // Determine package version.
+        if (cursor.isNull(1) || cursor.isNull(2)) {
+            Slog.e(TAG, "Package version information unexpectedly null");
+            return null;
+        }
+        PackageVersions packageVersions = new PackageVersions(cursor.getInt(1), cursor.getInt(2));
+
+        return new PackageStatus(checkStatus, packageVersions);
+    }
+
+    /**
+     * Generate a new {@link CheckToken} that can be passed to the time zone rules update
+     * application.
+     */
+    CheckToken generateCheckToken(PackageVersions currentInstalledVersions) {
+        if (currentInstalledVersions == null) {
+            throw new NullPointerException("currentInstalledVersions == null");
+        }
+
+        synchronized (this) {
+            Integer optimisticLockId = getCurrentOptimisticLockId();
+            if (optimisticLockId == null) {
+                Slog.w(TAG, "Unable to find optimistic lock ID from package status row");
+
+                // Recover.
+                optimisticLockId = mDatabaseHelper.recoverFromBadData();
+            }
+
+            int newOptimisticLockId = optimisticLockId + 1;
+            boolean statusRowUpdated = writeStatusRow(
+                    optimisticLockId, newOptimisticLockId, CHECK_STARTED, currentInstalledVersions);
+            if (!statusRowUpdated) {
+                Slog.e(TAG, "Unable to update status to CHECK_STARTED in package status row."
+                        + " synchronization failure?");
+                return null;
+            }
+            return new CheckToken(newOptimisticLockId, currentInstalledVersions);
+        }
+    }
+
+    /**
+     * Reset the current device state to "unknown".
+     */
+    void resetCheckState() {
+        synchronized(this) {
+            Integer optimisticLockId = getCurrentOptimisticLockId();
+            if (optimisticLockId == null) {
+                Slog.w(TAG, "resetCheckState: Unable to find optimistic lock ID from package"
+                        + " status row");
+                // Attempt to recover the storage state.
+                optimisticLockId = mDatabaseHelper.recoverFromBadData();
+            }
+
+            int newOptimisticLockId = optimisticLockId + 1;
+            if (!writeStatusRow(optimisticLockId, newOptimisticLockId,
+                    null /* status */, null /* packageVersions */)) {
+                Slog.e(TAG, "resetCheckState: Unable to reset package status row,"
+                        + " newOptimisticLockId=" + newOptimisticLockId);
+            }
+        }
+    }
+
+    /**
+     * Update the current device state if possible. Returns true if the update was successful.
+     * {@code false} indicates the storage has been changed since the {@link CheckToken} was
+     * generated and the update was discarded.
+     */
+    boolean markChecked(CheckToken checkToken, boolean succeeded) {
+        synchronized (this) {
+            int optimisticLockId = checkToken.mOptimisticLockId;
+            int newOptimisticLockId = optimisticLockId + 1;
+            int status = succeeded ? CHECK_COMPLETED_SUCCESS : CHECK_COMPLETED_FAILURE;
+            return writeStatusRow(optimisticLockId, newOptimisticLockId,
+                    status, checkToken.mPackageVersions);
+        }
+    }
+
+    // Caller should be synchronized(this)
+    private Integer getCurrentOptimisticLockId() {
+        final String[] columns = { COLUMN_OPTIMISTIC_LOCK_ID };
+        final String querySelection = COLUMN_ID + " = ?";
+        final String[] querySelectionArgs = { Integer.toString(SINGLETON_ID) };
+
+        SQLiteDatabase database = mDatabaseHelper.getReadableDatabase();
+        try (Cursor cursor = database.query(TABLE, columns, querySelection, querySelectionArgs,
+                null /* groupBy */, null /* having */, null /* orderBy */)) {
+            if (cursor.getCount() != 1) {
+                Slog.w(TAG, cursor.getCount() + " rows returned, expected exactly one.");
+                return null;
+            }
+            cursor.moveToFirst();
+            return cursor.getInt(0);
+        }
+    }
+
+    // Caller should be synchronized(this)
+    private boolean writeStatusRow(int optimisticLockId, int newOptimisticLockId, Integer status,
+            PackageVersions packageVersions) {
+        if ((status == null) != (packageVersions == null)) {
+            throw new IllegalArgumentException(
+                    "Provide both status and packageVersions, or neither.");
+        }
+
+        SQLiteDatabase database = mDatabaseHelper.getWritableDatabase();
+        ContentValues values = new ContentValues();
+        values.put(COLUMN_OPTIMISTIC_LOCK_ID, newOptimisticLockId);
+        if (status == null) {
+            values.putNull(COLUMN_CHECK_STATUS);
+            values.put(COLUMN_UPDATE_APP_VERSION, UNKNOWN_PACKAGE_VERSION);
+            values.put(COLUMN_DATA_APP_VERSION, UNKNOWN_PACKAGE_VERSION);
+        } else {
+            values.put(COLUMN_CHECK_STATUS, status);
+            values.put(COLUMN_UPDATE_APP_VERSION, packageVersions.mUpdateAppVersion);
+            values.put(COLUMN_DATA_APP_VERSION, packageVersions.mDataAppVersion);
+        }
+
+        String updateSelection = COLUMN_ID + " = ? AND " + COLUMN_OPTIMISTIC_LOCK_ID + " = ?";
+        String[] updateSelectionArgs = {
+                Integer.toString(SINGLETON_ID), Integer.toString(optimisticLockId)
+        };
+        int count = database.update(TABLE, values, updateSelection, updateSelectionArgs);
+        if (count > 1) {
+            // This has to be because of corruption: there should only ever be one row.
+            Slog.w(TAG, "writeStatusRow: " + count + " rows updated, expected exactly one.");
+            // Reset the table.
+            mDatabaseHelper.recoverFromBadData();
+        }
+
+        // 1 is the success case. 0 rows updated means the row is missing or the optimistic lock ID
+        // was not as expected, this could be because of corruption but is most likely due to an
+        // optimistic lock failure. Callers can decide on a case-by-case basis.
+        return count == 1;
+    }
+
+    /** Only used during tests to force an empty table. */
+    void deleteRowForTests() {
+        mDatabaseHelper.getWritableDatabase().delete(TABLE, null, null);
+    }
+
+    /** Only used during tests to force a known table state. */
+    public void forceCheckStateForTests(int checkStatus, PackageVersions packageVersions) {
+        int optimisticLockId = getCurrentOptimisticLockId();
+        writeStatusRow(optimisticLockId, optimisticLockId, checkStatus, packageVersions);
+    }
+
+    static class DatabaseHelper extends SQLiteOpenHelper {
+
+        private final Context mContext;
+
+        public DatabaseHelper(Context context) {
+            super(context, DATABASE_NAME, null, DATABASE_VERSION);
+            mContext = context;
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            db.execSQL("CREATE TABLE " + TABLE + " (" +
+                    "_id INTEGER PRIMARY KEY," +
+                    COLUMN_OPTIMISTIC_LOCK_ID + " INTEGER NOT NULL," +
+                    COLUMN_CHECK_STATUS + " INTEGER," +
+                    COLUMN_UPDATE_APP_VERSION + " INTEGER NOT NULL," +
+                    COLUMN_DATA_APP_VERSION + " INTEGER NOT NULL" +
+                    ");");
+            insertInitialRowState(db);
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) {
+            // no-op: nothing to upgrade
+        }
+
+        /** Recover the initial data row state, returning the new current optimistic lock ID */
+        int recoverFromBadData() {
+            // Delete the table content.
+            SQLiteDatabase writableDatabase = getWritableDatabase();
+            writableDatabase.delete(TABLE, null /* whereClause */, null /* whereArgs */);
+
+            // Insert the initial content.
+            return insertInitialRowState(writableDatabase);
+        }
+
+        /** Insert the initial data row, returning the optimistic lock ID */
+        private static int insertInitialRowState(SQLiteDatabase db) {
+            // Doesn't matter what it is, but we avoid the obvious starting value each time the row
+            // is reset to ensure that old tokens are unlikely to work.
+           final int initialOptimisticLockId = (int) System.currentTimeMillis();
+
+            // Insert the one row.
+            ContentValues values = new ContentValues();
+            values.put(COLUMN_ID, SINGLETON_ID);
+            values.put(COLUMN_OPTIMISTIC_LOCK_ID, initialOptimisticLockId);
+            values.putNull(COLUMN_CHECK_STATUS);
+            values.put(COLUMN_UPDATE_APP_VERSION, UNKNOWN_PACKAGE_VERSION);
+            values.put(COLUMN_DATA_APP_VERSION, UNKNOWN_PACKAGE_VERSION);
+            long id = db.insert(TABLE, null, values);
+            if (id == -1) {
+                Slog.w(TAG, "insertInitialRow: could not insert initial row, id=" + id);
+                return -1;
+            }
+            return initialOptimisticLockId;
+        }
+
+        File getDatabaseFile() {
+            return mContext.getDatabasePath(DATABASE_NAME);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/timezone/PackageTracker.java b/services/core/java/com/android/server/timezone/PackageTracker.java
new file mode 100644
index 0000000..8abf7df
--- /dev/null
+++ b/services/core/java/com/android/server/timezone/PackageTracker.java
@@ -0,0 +1,504 @@
+/*
+ * 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 com.android.internal.annotations.VisibleForTesting;
+
+import android.app.timezone.RulesUpdaterContract;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.provider.TimeZoneRulesDataContract;
+import android.util.Slog;
+
+/**
+ * Monitors the installed applications associated with time zone updates. If the app packages are
+ * updated it indicates there <em>might</em> be a time zone rules update to apply so a targeted
+ * broadcast intent is used to trigger the time zone updater app.
+ *
+ * <p>The "update triggering" behavior of this component can be disabled via device configuration.
+ *
+ * <p>The package tracker listens for package updates of the time zone "updater app" and "data app".
+ * It also listens for "reliability" triggers. Reliability triggers are there to ensure that the
+ * package tracker handles failures reliably and are "idle maintenance" events or something similar.
+ * Reliability triggers can cause a time zone update check to take place if the current state is
+ * unclear. For example, it can be unclear after boot or after a failure. If there are repeated
+ * failures reliability updates are halted until the next boot.
+ *
+ * <p>This component keeps persistent track of the most recent app packages checked to avoid
+ * unnecessary expense from broadcasting intents (which will cause other app processes to spawn).
+ * The current status is also stored to detect whether the most recently-generated check is
+ * complete successfully. For example, if the device was interrupted while doing a check and never
+ * acknowledged a check then a check will be retried the next time a "reliability trigger" event
+ * happens.
+ */
+// Also made non-final so it can be mocked.
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+public class PackageTracker implements IntentHelper.Listener {
+    private static final String TAG = "timezone.PackageTracker";
+
+    private final PackageManagerHelper mPackageManagerHelper;
+    private final IntentHelper mIntentHelper;
+    private final ConfigHelper mConfigHelper;
+    private final PackageStatusStorage mPackageStatusStorage;
+    private final ClockHelper mClockHelper;
+
+    // False if tracking is disabled.
+    private boolean mTrackingEnabled;
+
+    // These fields may be null if package tracking is disabled.
+    private String mUpdateAppPackageName;
+    private String mDataAppPackageName;
+
+    // The time a triggered check is allowed to take before it is considered overdue.
+    private int mCheckTimeAllowedMillis;
+    // The number of failed checks in a row before reliability checks should stop happening.
+    private long mFailedCheckRetryCount;
+
+    // Reliability check state: If a check was triggered but not acknowledged within
+    // mCheckTimeAllowedMillis then another one can be triggered.
+    private Long mLastTriggerTimestamp = null;
+
+    // Reliability check state: Whether any checks have been triggered at all.
+    private boolean mCheckTriggered;
+
+    // Reliability check state: A count of how many failures have occurred consecutively.
+    private int mCheckFailureCount;
+
+    /** Creates the {@link PackageTracker} for normal use. */
+    static PackageTracker create(Context context) {
+        PackageTrackerHelperImpl helperImpl = new PackageTrackerHelperImpl(context);
+        return new PackageTracker(
+                helperImpl /* clock */,
+                helperImpl /* configHelper */,
+                helperImpl /* packageManagerHelper */,
+                new PackageStatusStorage(context),
+                new IntentHelperImpl(context));
+    }
+
+    // A constructor that can be used by tests to supply mocked / faked dependencies.
+    PackageTracker(ClockHelper clockHelper, ConfigHelper configHelper,
+            PackageManagerHelper packageManagerHelper, PackageStatusStorage packageStatusStorage,
+            IntentHelper intentHelper) {
+        mClockHelper = clockHelper;
+        mConfigHelper = configHelper;
+        mPackageManagerHelper = packageManagerHelper;
+        mPackageStatusStorage = packageStatusStorage;
+        mIntentHelper = intentHelper;
+    }
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    protected synchronized void start() {
+        mTrackingEnabled = mConfigHelper.isTrackingEnabled();
+        if (!mTrackingEnabled) {
+            Slog.i(TAG, "Time zone updater / data package tracking explicitly disabled.");
+            return;
+        }
+
+        mUpdateAppPackageName = mConfigHelper.getUpdateAppPackageName();
+        mDataAppPackageName = mConfigHelper.getDataAppPackageName();
+        mCheckTimeAllowedMillis = mConfigHelper.getCheckTimeAllowedMillis();
+        mFailedCheckRetryCount = mConfigHelper.getFailedCheckRetryCount();
+
+        // Validate the device configuration including the application packages.
+        // The manifest entries in the apps themselves are not validated until use as they can
+        // change and we don't want to prevent the system server starting due to a bad application.
+        throwIfDeviceSettingsOrAppsAreBad();
+
+        // Explicitly start in a reliability state where reliability triggering will do something.
+        mCheckTriggered = false;
+        mCheckFailureCount = 0;
+
+        // Initialize the intent helper.
+        mIntentHelper.initialize(mUpdateAppPackageName, mDataAppPackageName, this);
+
+        // Enable the reliability triggering so we will have at least one reliability trigger if
+        // a package isn't updated.
+        mIntentHelper.enableReliabilityTriggering();
+
+        Slog.i(TAG, "Time zone updater / data package tracking enabled");
+    }
+
+    /**
+     * Performs checks that confirm the system image has correctly configured package
+     * tracking configuration. Only called if package tracking is enabled. Throws an exception if
+     * the device is configured badly which will prevent the device booting.
+     */
+    private void throwIfDeviceSettingsOrAppsAreBad() {
+        // None of the checks below can be based on application manifest settings, otherwise a bad
+        // update could leave the device in an unbootable state. See validateDataAppManifest() and
+        // validateUpdaterAppManifest() for softer errors.
+
+        throwRuntimeExceptionIfNullOrEmpty(
+                mUpdateAppPackageName, "Update app package name missing.");
+        throwRuntimeExceptionIfNullOrEmpty(mDataAppPackageName, "Data app package name missing.");
+        if (mFailedCheckRetryCount < 1) {
+            throw logAndThrowRuntimeException("mFailedRetryCount=" + mFailedCheckRetryCount, null);
+        }
+        if (mCheckTimeAllowedMillis < 1000) {
+            throw logAndThrowRuntimeException(
+                    "mCheckTimeAllowedMillis=" + mCheckTimeAllowedMillis, null);
+        }
+
+        // Validate the updater application package.
+        // TODO(nfuller) Uncomment or remove the code below. Currently an app stops being a priv-app
+        // after it is replaced by one in data so this check fails. http://b/35995024
+        // try {
+        //     if (!mPackageManagerHelper.isPrivilegedApp(mUpdateAppPackageName)) {
+        //         throw failWithException(
+        //                 "Update app " + mUpdateAppPackageName + " must be a priv-app.", null);
+        //     }
+        // } catch (PackageManager.NameNotFoundException e) {
+        //     throw failWithException("Could not determine update app package details for "
+        //             + mUpdateAppPackageName, e);
+        // }
+        // TODO(nfuller) Consider permission checks. While an updated system app retains permissions
+        // obtained by the system version it's not clear how to check them.
+        Slog.d(TAG, "Update app " + mUpdateAppPackageName + " is valid.");
+
+        // Validate the data application package.
+        // TODO(nfuller) Uncomment or remove the code below. Currently an app stops being a priv-app
+        // after it is replaced by one in data. http://b/35995024
+        // try {
+        //     if (!mPackageManagerHelper.isPrivilegedApp(mDataAppPackageName)) {
+        //         throw failWithException(
+        //                 "Data app " + mDataAppPackageName + " must be a priv-app.", null);
+        //     }
+        // } catch (PackageManager.NameNotFoundException e) {
+        //     throw failWithException("Could not determine data app package details for "
+        //             + mDataAppPackageName, e);
+        // }
+        // TODO(nfuller) Consider permission checks. While an updated system app retains permissions
+        // obtained by the system version it's not clear how to check them.
+        Slog.d(TAG, "Data app " + mDataAppPackageName + " is valid.");
+    }
+
+    /**
+     * Inspects the current in-memory state, installed packages and storage state to determine if an
+     * update check is needed and then trigger if it is.
+     *
+     * @param packageChanged true if this method was called because a known packaged definitely
+     *     changed, false if the cause is a reliability trigger
+     */
+    @Override
+    public synchronized void triggerUpdateIfNeeded(boolean packageChanged) {
+        if (!mTrackingEnabled) {
+            throw new IllegalStateException("Unexpected call. Tracking is disabled.");
+        }
+
+        // Validate the applications' current manifest entries: make sure they are configured as
+        // they should be. These are not fatal and just means that no update is triggered: we don't
+        // want to take down the system server if an OEM or Google have pushed a bad update to
+        // an application.
+        boolean updaterAppManifestValid = validateUpdaterAppManifest();
+        boolean dataAppManifestValid = validateDataAppManifest();
+        if (!updaterAppManifestValid || !dataAppManifestValid) {
+            Slog.e(TAG, "No update triggered due to invalid application manifest entries."
+                    + " updaterApp=" + updaterAppManifestValid
+                    + ", dataApp=" + dataAppManifestValid);
+
+            // There's no point in doing reliability checks if the current packages are bad.
+            mIntentHelper.disableReliabilityTriggering();
+            return;
+        }
+
+        if (!packageChanged) {
+            // This call was made because the device is doing a "reliability" check.
+            // 4 possible cases:
+            // 1) No check has previously triggered since restart. We want to trigger in this case.
+            // 2) A check has previously triggered and it is in progress. We want to trigger if
+            //    the response is overdue.
+            // 3) A check has previously triggered and it failed. We want to trigger, but only if
+            //    we're not in a persistent failure state.
+            // 4) A check has previously triggered and it succeeded.
+            //    We don't want to trigger, and want to stop future triggers.
+
+            if (!mCheckTriggered) {
+                // Case 1.
+                Slog.d(TAG, "triggerUpdateIfNeeded: First reliability trigger.");
+            } else if (isCheckInProgress()) {
+                // Case 2.
+                if (!isCheckResponseOverdue()) {
+                    // A check is in progress but hasn't been given time to succeed.
+                    Slog.d(TAG,
+                            "triggerUpdateIfNeeded: checkComplete call is not yet overdue."
+                                    + " Not triggering.");
+                    // Not doing any work, but also not disabling future reliability triggers.
+                    return;
+                }
+            } else if (mCheckFailureCount > mFailedCheckRetryCount) {
+                // Case 3. If the system is in some kind of persistent failure state we don't want
+                // to keep checking, so just stop.
+                Slog.i(TAG, "triggerUpdateIfNeeded: number of allowed consecutive check failures"
+                        + " exceeded. Stopping reliability triggers until next reboot or package"
+                        + " update.");
+                mIntentHelper.disableReliabilityTriggering();
+                return;
+            } else if (mCheckFailureCount == 0) {
+                // Case 4.
+                Slog.i(TAG, "triggerUpdateIfNeeded: No reliability check required. Last check was"
+                        + " successful.");
+                mIntentHelper.disableReliabilityTriggering();
+                return;
+            }
+        }
+
+        // Read the currently installed data / updater package versions.
+        PackageVersions currentInstalledVersions = lookupInstalledPackageVersions();
+        if (currentInstalledVersions == null) {
+            // This should not happen if the device is configured in a valid way.
+            Slog.e(TAG, "triggerUpdateIfNeeded: currentInstalledVersions was null");
+            mIntentHelper.disableReliabilityTriggering();
+            return;
+        }
+
+        // Establish the current state using package manager and stored state. Determine if we have
+        // already successfully checked the installed versions.
+        PackageStatus packageStatus = mPackageStatusStorage.getPackageStatus();
+        if (packageStatus == null) {
+            // This can imply corrupt, uninitialized storage state (e.g. first check ever on a
+            // device) or after some kind of reset.
+            Slog.i(TAG, "triggerUpdateIfNeeded: No package status data found. Data check needed.");
+        } else if (!packageStatus.mVersions.equals(currentInstalledVersions)) {
+            // The stored package version information differs from the installed version.
+            // Trigger the check in all cases.
+            Slog.i(TAG, "triggerUpdateIfNeeded: Stored package versions="
+                    + packageStatus.mVersions + ", do not match current package versions="
+                    + currentInstalledVersions + ". Triggering check.");
+        } else {
+            Slog.i(TAG, "triggerUpdateIfNeeded: Stored package versions match currently"
+                    + " installed versions, currentInstalledVersions=" + currentInstalledVersions
+                    + ", packageStatus.mCheckStatus=" + packageStatus.mCheckStatus);
+            if (packageStatus.mCheckStatus == PackageStatus.CHECK_COMPLETED_SUCCESS) {
+                // The last check succeeded and nothing has changed. Do nothing and disable
+                // reliability checks.
+                Slog.i(TAG, "triggerUpdateIfNeeded: Prior check succeeded. No need to trigger.");
+                mIntentHelper.disableReliabilityTriggering();
+                return;
+            }
+        }
+
+        // Generate a token to send to the updater app.
+        CheckToken checkToken =
+                mPackageStatusStorage.generateCheckToken(currentInstalledVersions);
+        if (checkToken == null) {
+            Slog.w(TAG, "triggerUpdateIfNeeded: Unable to generate check token."
+                    + " Not sending check request.");
+            return;
+        }
+
+        // Trigger the update check.
+        mIntentHelper.sendTriggerUpdateCheck(checkToken);
+        mCheckTriggered = true;
+
+        // Update the reliability check state in case the update fails.
+        setCheckInProgress();
+
+        // Enable reliability triggering in case the check doesn't succeed and there is no
+        // response at all. Enabling reliability triggering is idempotent.
+        mIntentHelper.enableReliabilityTriggering();
+    }
+
+    /**
+     * Used to record the result of a check. Can be called even if active package tracking is
+     * disabled.
+     */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    protected synchronized void recordCheckResult(CheckToken checkToken, boolean success) {
+        Slog.i(TAG, "recordOperationResult: checkToken=" + checkToken + " success=" + success);
+
+        // If package tracking is disabled it means no record-keeping is required. However, we do
+        // want to clear out any stored state to make it clear that the current state is unknown and
+        // should tracking become enabled again (perhaps through an OTA) we'd need to perform an
+        // update check.
+        if (!mTrackingEnabled) {
+            // This means an updater has spontaneously modified time zone data without having been
+            // triggered. This can happen if the OEM is handling their own updates, but we don't
+            // need to do any tracking in this case.
+
+            if (checkToken == null) {
+                // This is the expected case if tracking is disabled but an OEM is handling time
+                // zone installs using their own mechanism.
+                Slog.d(TAG, "recordCheckResult: Tracking is disabled and no token has been"
+                        + " provided. Resetting tracking state.");
+            } else {
+                // This is unexpected. If tracking is disabled then no check token should have been
+                // generated by the package tracker. An updater should never create its own token.
+                // This could be a bug in the updater.
+                Slog.w(TAG, "recordCheckResult: Tracking is disabled and a token " + checkToken
+                        + " has been unexpectedly provided. Resetting tracking state.");
+            }
+            mPackageStatusStorage.resetCheckState();
+            return;
+        }
+
+        if (checkToken == null) {
+            /*
+             * If the checkToken is null it suggests an install / uninstall / acknowledgement has
+             * occurred without a prior trigger (or the client didn't return the token it was given
+             * for some reason, perhaps a bug).
+             *
+             * This shouldn't happen under normal circumstances:
+             *
+             * If package tracking is enabled, we assume it is the package tracker responsible for
+             * triggering updates and a token should have been produced and returned.
+             *
+             * If the OEM is handling time zone updates case package tracking should be disabled.
+             *
+             * This could happen in tests. The device should recover back to a known state by
+             * itself rather than be left in an invalid state.
+             *
+             * We treat this as putting the device into an unknown state and make sure that
+             * reliability triggering is enabled so we should recover.
+             */
+            Slog.i(TAG, "recordCheckResult: Unexpectedly missing checkToken, resetting"
+                    + " storage state.");
+            mPackageStatusStorage.resetCheckState();
+
+            // Enable reliability triggering and reset the failure count so we know that the
+            // next reliability trigger will do something.
+            mIntentHelper.enableReliabilityTriggering();
+            mCheckFailureCount = 0;
+        } else {
+            // This is the expected case when tracking is enabled: a check was triggered and it has
+            // completed.
+            boolean recordedCheckCompleteSuccessfully =
+                    mPackageStatusStorage.markChecked(checkToken, success);
+            if (recordedCheckCompleteSuccessfully) {
+                // If we have recorded the result (whatever it was) we know there is no check in
+                // progress.
+                setCheckComplete();
+
+                if (success) {
+                    // Since the check was successful, no more reliability checks are required until
+                    // there is a package change.
+                    mIntentHelper.disableReliabilityTriggering();
+                    mCheckFailureCount = 0;
+                } else {
+                    // Enable reliability triggering to potentially check again in future.
+                    mIntentHelper.enableReliabilityTriggering();
+                    mCheckFailureCount++;
+                }
+            } else {
+                // The failure to record the check means an optimistic lock failure and suggests
+                // that another check was triggered after the token was generated.
+                Slog.i(TAG, "recordCheckResult: could not update token=" + checkToken
+                        + " with success=" + success + ". Optimistic lock failure");
+
+                // Enable reliability triggering to potentially try again in future.
+                mIntentHelper.enableReliabilityTriggering();
+                mCheckFailureCount++;
+            }
+        }
+    }
+
+    /** Access to consecutive failure counts for use in tests. */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    protected int getCheckFailureCountForTests() {
+        return mCheckFailureCount;
+    }
+
+    private void setCheckInProgress() {
+        mLastTriggerTimestamp = mClockHelper.currentTimestamp();
+    }
+
+    private void setCheckComplete() {
+        mLastTriggerTimestamp = null;
+    }
+
+    private boolean isCheckInProgress() {
+        return mLastTriggerTimestamp != null;
+    }
+
+    private boolean isCheckResponseOverdue() {
+        if (mLastTriggerTimestamp == null) {
+            return false;
+        }
+        // Risk of overflow, but highly unlikely given the implementation and not problematic.
+        return mClockHelper.currentTimestamp() > mLastTriggerTimestamp + mCheckTimeAllowedMillis;
+    }
+
+    private PackageVersions lookupInstalledPackageVersions() {
+        int updatePackageVersion;
+        int dataPackageVersion;
+        try {
+            updatePackageVersion =
+                    mPackageManagerHelper.getInstalledPackageVersion(mUpdateAppPackageName);
+            dataPackageVersion =
+                    mPackageManagerHelper.getInstalledPackageVersion(mDataAppPackageName);
+        } catch (PackageManager.NameNotFoundException e) {
+            Slog.w(TAG, "lookupInstalledPackageVersions: Unable to resolve installed package"
+                    + " versions", e);
+            return null;
+        }
+        return new PackageVersions(updatePackageVersion, dataPackageVersion);
+    }
+
+    private boolean validateDataAppManifest() {
+        // We only want to talk to a provider that exposed by the known data app package
+        // so we look up the providers exposed by that app and check the well-known authority is
+        // there. This prevents the case where *even if* the data app doesn't expose the provider
+        // required, another app cannot expose one to replace it.
+        if (!mPackageManagerHelper.contentProviderRegistered(
+                TimeZoneRulesDataContract.AUTHORITY, mDataAppPackageName)) {
+            // Error! Found the package but it didn't expose the correct provider.
+            Slog.w(TAG, "validateDataAppManifest: Data app " + mDataAppPackageName
+                    + " does not expose the required provider with authority="
+                    + TimeZoneRulesDataContract.AUTHORITY);
+            return false;
+        }
+        // TODO(nfuller) Add any permissions checks needed.
+        return true;
+    }
+
+    private boolean validateUpdaterAppManifest() {
+        try {
+            // The updater app is expected to have the UPDATE_TIME_ZONE_RULES permission.
+            // The updater app is expected to have a receiver for the intent we are going to trigger
+            // and require the TRIGGER_TIME_ZONE_RULES_CHECK.
+            if (!mPackageManagerHelper.usesPermission(
+                    mUpdateAppPackageName,
+                    RulesUpdaterContract.UPDATE_TIME_ZONE_RULES_PERMISSION)) {
+                Slog.w(TAG, "validateUpdaterAppManifest: Updater app " + mDataAppPackageName
+                        + " does not use permission="
+                        + RulesUpdaterContract.UPDATE_TIME_ZONE_RULES_PERMISSION);
+                return false;
+            }
+            if (!mPackageManagerHelper.receiverRegistered(
+                    RulesUpdaterContract.createUpdaterIntent(mUpdateAppPackageName),
+                    RulesUpdaterContract.TRIGGER_TIME_ZONE_RULES_CHECK_PERMISSION)) {
+                return false;
+            }
+
+            return true;
+        } catch (PackageManager.NameNotFoundException e) {
+            Slog.w(TAG, "validateUpdaterAppManifest: Updater app " + mDataAppPackageName
+                    + " does not expose the required broadcast receiver.", e);
+            return false;
+        }
+    }
+
+    private static void throwRuntimeExceptionIfNullOrEmpty(String value, String message) {
+        if (value == null || value.trim().isEmpty()) {
+            throw logAndThrowRuntimeException(message, null);
+        }
+    }
+
+    private static RuntimeException logAndThrowRuntimeException(String message, Throwable cause) {
+        Slog.wtf(TAG, message, cause);
+        throw new RuntimeException(message, cause);
+    }
+}
diff --git a/services/core/java/com/android/server/timezone/PackageTrackerHelperImpl.java b/services/core/java/com/android/server/timezone/PackageTrackerHelperImpl.java
new file mode 100644
index 0000000..2e0c21b
--- /dev/null
+++ b/services/core/java/com/android/server/timezone/PackageTrackerHelperImpl.java
@@ -0,0 +1,154 @@
+/*
+ * 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 com.android.internal.R;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.os.SystemClock;
+import android.util.Slog;
+
+import java.util.List;
+
+/**
+ * A single class that implements multiple helper interfaces for use by {@link PackageTracker}.
+ */
+final class PackageTrackerHelperImpl implements ClockHelper, ConfigHelper, PackageManagerHelper {
+
+    private static final String TAG = "PackageTrackerHelperImpl";
+
+    private final Context mContext;
+    private final PackageManager mPackageManager;
+
+    PackageTrackerHelperImpl(Context context) {
+        mContext = context;
+        mPackageManager = context.getPackageManager();
+    }
+
+    @Override
+    public boolean isTrackingEnabled() {
+        return mContext.getResources().getBoolean(R.bool.config_timeZoneRulesUpdateTrackingEnabled);
+    }
+
+    @Override
+    public String getUpdateAppPackageName() {
+        return mContext.getResources().getString(R.string.config_timeZoneRulesUpdaterPackage);
+    }
+
+    @Override
+    public String getDataAppPackageName() {
+        Resources resources = mContext.getResources();
+        return resources.getString(R.string.config_timeZoneRulesDataPackage);
+    }
+
+    @Override
+    public int getCheckTimeAllowedMillis() {
+        return mContext.getResources().getInteger(
+                R.integer.config_timeZoneRulesCheckTimeMillisAllowed);
+    }
+
+    @Override
+    public int getFailedCheckRetryCount() {
+        return mContext.getResources().getInteger(R.integer.config_timeZoneRulesCheckRetryCount);
+    }
+
+    @Override
+    public long currentTimestamp() {
+        // Use of elapsedRealtime() because this is in-memory state and elapsedRealtime() shouldn't
+        // change if the system clock changes.
+        return SystemClock.elapsedRealtime();
+    }
+
+    @Override
+    public int getInstalledPackageVersion(String packageName)
+            throws PackageManager.NameNotFoundException {
+        int flags = PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
+        PackageInfo packageInfo = mPackageManager.getPackageInfo(packageName, flags);
+        return packageInfo.versionCode;
+    }
+
+    @Override
+    public boolean isPrivilegedApp(String packageName) throws PackageManager.NameNotFoundException {
+        int flags = PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
+        PackageInfo packageInfo = mPackageManager.getPackageInfo(packageName, flags);
+        return packageInfo.applicationInfo.isPrivilegedApp();
+    }
+
+    @Override
+    public boolean usesPermission(String packageName, String requiredPermissionName)
+            throws PackageManager.NameNotFoundException {
+        int flags = PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS
+                | PackageManager.GET_PERMISSIONS;
+        PackageInfo packageInfo = mPackageManager.getPackageInfo(packageName, flags);
+        if (packageInfo.requestedPermissions == null) {
+            return false;
+        }
+        for (String requestedPermission : packageInfo.requestedPermissions) {
+            if (requiredPermissionName.equals(requestedPermission)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean contentProviderRegistered(String authority, String requiredPackageName) {
+        int flags = PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
+        ProviderInfo providerInfo =
+                mPackageManager.resolveContentProvider(authority, flags);
+        if (providerInfo == null) {
+            Slog.i(TAG, "contentProviderRegistered: No content provider registered with authority="
+                    + authority);
+            return false;
+        }
+        boolean packageMatches =
+                requiredPackageName.equals(providerInfo.applicationInfo.packageName);
+        if (!packageMatches) {
+            Slog.i(TAG, "contentProviderRegistered: App with packageName=" + requiredPackageName
+                    + " does not expose the a content provider with authority=" + authority);
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public boolean receiverRegistered(Intent intent, String requiredPermissionName)
+            throws PackageManager.NameNotFoundException {
+
+        int flags = PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
+        List<ResolveInfo> resolveInfo = mPackageManager.queryBroadcastReceivers(intent, flags);
+        if (resolveInfo.size() != 1) {
+            Slog.i(TAG, "receiverRegistered: Zero or multiple broadcast receiver registered for"
+                    + " intent=" + intent + ", found=" + resolveInfo);
+            return false;
+        }
+
+        ResolveInfo matched = resolveInfo.get(0);
+        boolean requiresPermission = requiredPermissionName.equals(matched.activityInfo.permission);
+        if (!requiresPermission) {
+            Slog.i(TAG, "receiverRegistered: Broadcast receiver registered for intent="
+                    + intent + " must require permission " + requiredPermissionName);
+        }
+        return requiresPermission;
+    }
+}
diff --git a/services/core/java/com/android/server/timezone/PackageVersions.java b/services/core/java/com/android/server/timezone/PackageVersions.java
new file mode 100644
index 0000000..fc0d6e1
--- /dev/null
+++ b/services/core/java/com/android/server/timezone/PackageVersions.java
@@ -0,0 +1,63 @@
+/*
+ * 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;
+
+/**
+ * Package version information about the time zone updater and time zone data application packages.
+ */
+final class PackageVersions {
+
+    final int mUpdateAppVersion;
+    final int mDataAppVersion;
+
+    PackageVersions(int updateAppVersion, int dataAppVersion) {
+        this.mUpdateAppVersion = updateAppVersion;
+        this.mDataAppVersion = dataAppVersion;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        PackageVersions that = (PackageVersions) o;
+
+        if (mUpdateAppVersion != that.mUpdateAppVersion) {
+            return false;
+        }
+        return mDataAppVersion == that.mDataAppVersion;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = mUpdateAppVersion;
+        result = 31 * result + mDataAppVersion;
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "PackageVersions{" +
+                "mUpdateAppVersion=" + mUpdateAppVersion +
+                ", mDataAppVersion=" + mDataAppVersion +
+                '}';
+    }
+}
diff --git a/services/core/java/com/android/server/timezone/PermissionHelper.java b/services/core/java/com/android/server/timezone/PermissionHelper.java
new file mode 100644
index 0000000..ba91c7f
--- /dev/null
+++ b/services/core/java/com/android/server/timezone/PermissionHelper.java
@@ -0,0 +1,25 @@
+/*
+ * 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;
+
+/**
+ * An easy-to-mock interface around permission checks for use by {@link RulesManagerService}.
+ */
+public interface PermissionHelper {
+
+    void enforceCallerHasPermission(String requiredPermission) throws SecurityException;
+}
diff --git a/services/core/java/com/android/server/timezone/RulesManagerService.java b/services/core/java/com/android/server/timezone/RulesManagerService.java
new file mode 100644
index 0000000..82bd356
--- /dev/null
+++ b/services/core/java/com/android/server/timezone/RulesManagerService.java
@@ -0,0 +1,348 @@
+/*
+ * 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 com.android.internal.annotations.VisibleForTesting;
+import com.android.server.SystemService;
+
+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 java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+import libcore.tzdata.shared2.DistroException;
+import libcore.tzdata.shared2.DistroVersion;
+import libcore.tzdata.shared2.StagedDistroOperation;
+import libcore.tzdata.update2.TimeZoneDistroInstaller;
+
+// TODO(nfuller) Add EventLog calls where useful in the system server.
+// TODO(nfuller) Check logging best practices in the system server.
+// TODO(nfuller) Check error handling best practices in the system server.
+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(
+                    DistroVersion.CURRENT_FORMAT_MAJOR_VERSION,
+                    DistroVersion.CURRENT_FORMAT_MINOR_VERSION);
+
+    public static class Lifecycle extends SystemService {
+        private RulesManagerService mService;
+
+        public Lifecycle(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void onStart() {
+            mService = RulesManagerService.create(getContext());
+            mService.start();
+
+            publishBinderService(Context.TIME_ZONE_RULES_MANAGER_SERVICE, mService);
+        }
+    }
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    static final String REQUIRED_UPDATER_PERMISSION =
+            android.Manifest.permission.UPDATE_TIME_ZONE_RULES;
+    private static final File SYSTEM_TZ_DATA_FILE = new File("/system/usr/share/zoneinfo/tzdata");
+    private static final File TZ_DATA_DIR = new File("/data/misc/zoneinfo");
+
+    private final AtomicBoolean mOperationInProgress = new AtomicBoolean(false);
+    private final PermissionHelper mPermissionHelper;
+    private final PackageTracker mPackageTracker;
+    private final Executor mExecutor;
+    private final TimeZoneDistroInstaller mInstaller;
+    private final FileDescriptorHelper mFileDescriptorHelper;
+
+    private static RulesManagerService create(Context context) {
+        RulesManagerServiceHelperImpl helper = new RulesManagerServiceHelperImpl(context);
+        return new RulesManagerService(
+                helper /* permissionHelper */,
+                helper /* executor */,
+                helper /* fileDescriptorHelper */,
+                PackageTracker.create(context),
+                new TimeZoneDistroInstaller(TAG, SYSTEM_TZ_DATA_FILE, TZ_DATA_DIR));
+    }
+
+    // A constructor that can be used by tests to supply mocked / faked dependencies.
+    RulesManagerService(PermissionHelper permissionHelper,
+            Executor executor,
+            FileDescriptorHelper fileDescriptorHelper, PackageTracker packageTracker,
+            TimeZoneDistroInstaller timeZoneDistroInstaller) {
+        mPermissionHelper = permissionHelper;
+        mExecutor = executor;
+        mFileDescriptorHelper = fileDescriptorHelper;
+        mPackageTracker = packageTracker;
+        mInstaller = timeZoneDistroInstaller;
+    }
+
+    public void start() {
+        mPackageTracker.start();
+    }
+
+    @Override // Binder call
+    public RulesState getRulesState() {
+        mPermissionHelper.enforceCallerHasPermission(REQUIRED_UPDATER_PERMISSION);
+
+        synchronized(this) {
+            String systemRulesVersion;
+            try {
+                systemRulesVersion = mInstaller.getSystemRulesVersion();
+            } catch (IOException e) {
+                Slog.w(TAG, "Failed to read system rules", e);
+                return null;
+            }
+
+            boolean operationInProgress = this.mOperationInProgress.get();
+
+            // Determine the staged operation status, if possible.
+            DistroRulesVersion stagedDistroRulesVersion = null;
+            int stagedOperationStatus = RulesState.STAGED_OPERATION_UNKNOWN;
+            if (!operationInProgress) {
+                StagedDistroOperation stagedDistroOperation;
+                try {
+                    stagedDistroOperation = mInstaller.getStagedDistroOperation();
+                    if (stagedDistroOperation == null) {
+                        stagedOperationStatus = RulesState.STAGED_OPERATION_NONE;
+                    } else if (stagedDistroOperation.isUninstall) {
+                        stagedOperationStatus = RulesState.STAGED_OPERATION_UNINSTALL;
+                    } else {
+                        // Must be an install.
+                        stagedOperationStatus = RulesState.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);
+                }
+            }
+
+            // Determine the installed distro state, if possible.
+            DistroVersion installedDistroVersion;
+            int distroStatus = RulesState.DISTRO_STATUS_UNKNOWN;
+            DistroRulesVersion installedDistroRulesVersion = null;
+            if (!operationInProgress) {
+                try {
+                    installedDistroVersion = mInstaller.getInstalledDistroVersion();
+                    if (installedDistroVersion == null) {
+                        distroStatus = RulesState.DISTRO_STATUS_NONE;
+                        installedDistroRulesVersion = null;
+                    } else {
+                        distroStatus = RulesState.DISTRO_STATUS_INSTALLED;
+                        installedDistroRulesVersion = new DistroRulesVersion(
+                                installedDistroVersion.rulesVersion,
+                                installedDistroVersion.revision);
+                    }
+                } catch (DistroException | IOException e) {
+                    Slog.w(TAG, "Failed to read installed distro.", e);
+                }
+            }
+            return new RulesState(systemRulesVersion, DISTRO_FORMAT_VERSION_SUPPORTED,
+                    operationInProgress, stagedOperationStatus, stagedDistroRulesVersion,
+                    distroStatus, installedDistroRulesVersion);
+        }
+    }
+
+    @Override
+    public int requestInstall(
+            ParcelFileDescriptor timeZoneDistro, byte[] checkTokenBytes, ICallback callback) {
+        mPermissionHelper.enforceCallerHasPermission(REQUIRED_UPDATER_PERMISSION);
+
+        CheckToken checkToken = null;
+        if (checkTokenBytes != null) {
+            checkToken = createCheckTokenOrThrow(checkTokenBytes);
+        }
+        synchronized (this) {
+            if (timeZoneDistro == null) {
+                throw new NullPointerException("timeZoneDistro == 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(timeZoneDistro, checkToken, callback));
+
+            return RulesManager.SUCCESS;
+        }
+    }
+
+    private class InstallRunnable implements Runnable {
+
+        private final ParcelFileDescriptor mTimeZoneDistro;
+        private final CheckToken mCheckToken;
+        private final ICallback mCallback;
+
+        InstallRunnable(
+                ParcelFileDescriptor timeZoneDistro, CheckToken checkToken, ICallback callback) {
+            mTimeZoneDistro = timeZoneDistro;
+            mCheckToken = checkToken;
+            mCallback = callback;
+        }
+
+        @Override
+        public void run() {
+            // Adopt the ParcelFileDescriptor into this try-with-resources so it is closed
+            // when we are done.
+            boolean success = false;
+            try {
+                byte[] distroBytes =
+                        RulesManagerService.this.mFileDescriptorHelper.readFully(mTimeZoneDistro);
+                int installerResult = mInstaller.stageInstallWithErrorCode(distroBytes);
+                int resultCode = mapInstallerResultToApiCode(installerResult);
+                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);
+                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 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);
+        }
+        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;
+
+        public UninstallRunnable(CheckToken checkToken, ICallback callback) {
+            mCheckToken = checkToken;
+            mCallback = callback;
+        }
+
+        @Override
+        public void run() {
+            boolean success = false;
+            try {
+                success = mInstaller.stageUninstall();
+                // Right now we just have success (0) / failure (1). All clients should be checking
+                // against SUCCESS. More granular failures may be added in future.
+                int resultCode = success ? Callback.SUCCESS
+                        : Callback.ERROR_UNKNOWN_FAILURE;
+                sendFinishedStatus(mCallback, resultCode);
+            } catch (Exception e) {
+                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, success);
+
+                mOperationInProgress.set(false);
+            }
+        }
+    }
+
+    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);
+        }
+        mPackageTracker.recordCheckResult(checkToken, success);
+    }
+
+    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;
+    }
+}
diff --git a/services/core/java/com/android/server/timezone/RulesManagerServiceHelperImpl.java b/services/core/java/com/android/server/timezone/RulesManagerServiceHelperImpl.java
new file mode 100644
index 0000000..15a571d
--- /dev/null
+++ b/services/core/java/com/android/server/timezone/RulesManagerServiceHelperImpl.java
@@ -0,0 +1,59 @@
+/*
+ * 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 android.content.Context;
+import android.os.ParcelFileDescriptor;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.concurrent.Executor;
+import libcore.io.Streams;
+
+/**
+ * A single class that implements multiple helper interfaces for use by {@link RulesManagerService}.
+ */
+final class RulesManagerServiceHelperImpl
+        implements PermissionHelper, Executor, FileDescriptorHelper {
+
+    private final Context mContext;
+
+    RulesManagerServiceHelperImpl(Context context) {
+        mContext = context;
+    }
+
+    @Override
+    public void enforceCallerHasPermission(String requiredPermission) {
+        mContext.enforceCallingPermission(requiredPermission, null /* message */);
+    }
+
+    // TODO Wake lock required?
+    @Override
+    public void execute(Runnable runnable) {
+        // TODO Is there a better way?
+        new Thread(runnable).start();
+    }
+
+    @Override
+    public byte[] readFully(ParcelFileDescriptor parcelFileDescriptor) throws IOException {
+        try (ParcelFileDescriptor pfd = parcelFileDescriptor) {
+            // Read bytes
+            FileInputStream in = new FileInputStream(pfd.getFileDescriptor(), false /* isOwner */);
+            return Streams.readFully(in);
+        }
+    }
+}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index b4e806b..e1cbc91 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -171,6 +171,8 @@
             "com.android.server.content.ContentService$Lifecycle";
     private static final String WALLPAPER_SERVICE_CLASS =
             "com.android.server.wallpaper.WallpaperManagerService$Lifecycle";
+    private static final String TIME_ZONE_RULES_MANAGER_SERVICE_CLASS =
+            "com.android.server.timezone.RulesManagerService$Lifecycle";
 
     private static final String PERSISTENT_DATA_BLOCK_PROP = "ro.frp.pst";
 
@@ -978,6 +980,13 @@
                 Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
             }
 
+            if (!disableNonCoreServices && context.getResources().getBoolean(
+                        R.bool.config_enableUpdateableTimeZoneRules)) {
+                traceBeginAndSlog("StartTimeZoneRulesManagerService");
+                mSystemServiceManager.startService(TIME_ZONE_RULES_MANAGER_SERVICE_CLASS);
+                Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
+            }
+
             traceBeginAndSlog("StartAudioService");
             mSystemServiceManager.startService(AudioService.Lifecycle.class);
             Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
diff --git a/services/tests/servicestests/src/com/android/server/timezone/CheckTokenTest.java b/services/tests/servicestests/src/com/android/server/timezone/CheckTokenTest.java
new file mode 100644
index 0000000..9603a06
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/timezone/CheckTokenTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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 org.junit.Test;
+
+import android.support.test.filters.SmallTest;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
+
+@SmallTest
+public class CheckTokenTest {
+
+    @Test
+    public void toByteArray() throws Exception {
+        PackageVersions packageVersions =
+                new PackageVersions(1 /* updateAppVersion */, 1 /* dataAppVersion */);
+        CheckToken originalToken = new CheckToken(1 /* optimisticLockId */, packageVersions);
+        assertEquals(originalToken, CheckToken.fromByteArray(originalToken.toByteArray()));
+    }
+
+    @Test
+    public void fromByteArray() {
+        PackageVersions packageVersions =
+                new PackageVersions(1 /* updateAppVersion */, 1 /* dataAppVersion */);
+        CheckToken token = new CheckToken(1, packageVersions);
+        byte[] validTokenBytes = token.toByteArray();
+        byte[] shortTokenBytes = new byte[validTokenBytes.length - 1];
+        System.arraycopy(validTokenBytes, 0, shortTokenBytes, 0, shortTokenBytes.length);
+
+        try {
+            CheckToken.fromByteArray(shortTokenBytes);
+            fail();
+        } catch (IOException expected) {}
+    }
+
+    @Test
+    public void equals() {
+        PackageVersions packageVersions1 =
+                new PackageVersions(1 /* updateAppVersion */, 1 /* dataAppVersion */);
+        PackageVersions packageVersions2 =
+                new PackageVersions(2 /* updateAppVersion */, 2 /* dataAppVersion */);
+        assertFalse(packageVersions1.equals(packageVersions2));
+
+        CheckToken baseline = new CheckToken(1, packageVersions1);
+        assertEquals(baseline, baseline);
+
+        CheckToken deepEqual = new CheckToken(1, packageVersions1);
+        assertEquals(baseline, deepEqual);
+
+        CheckToken differentOptimisticLockId = new CheckToken(2, packageVersions1);
+        assertFalse(differentOptimisticLockId.equals(baseline));
+
+        CheckToken differentPackageVersions = new CheckToken(1, packageVersions2);
+        assertFalse(differentPackageVersions.equals(baseline));
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/timezone/PackageStatusStorageTest.java b/services/tests/servicestests/src/com/android/server/timezone/PackageStatusStorageTest.java
new file mode 100644
index 0000000..e085270
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/timezone/PackageStatusStorageTest.java
@@ -0,0 +1,229 @@
+/*
+ * 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 org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+
+import static junit.framework.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+@SmallTest
+public class PackageStatusStorageTest {
+    private static final PackageVersions VALID_PACKAGE_VERSIONS = new PackageVersions(1, 2);
+
+    private PackageStatusStorage mPackageStatusStorage;
+
+    @Before
+    public void setUp() throws Exception {
+        Context context = InstrumentationRegistry.getContext();
+
+        // Using the instrumentation context means the database is created in a test app-specific
+        // directory.
+        mPackageStatusStorage = new PackageStatusStorage(context);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mPackageStatusStorage.deleteDatabaseForTests();
+    }
+
+    @Test
+    public void getPackageStatus_initialState() {
+        assertNull(mPackageStatusStorage.getPackageStatus());
+    }
+
+    @Test
+    public void resetCheckState() {
+        // Assert initial state.
+        assertNull(mPackageStatusStorage.getPackageStatus());
+
+        CheckToken token1 = mPackageStatusStorage.generateCheckToken(VALID_PACKAGE_VERSIONS);
+
+        // There should now be a state.
+        assertNotNull(mPackageStatusStorage.getPackageStatus());
+
+        // Now clear the state.
+        mPackageStatusStorage.resetCheckState();
+
+        // After reset, there should be no package state again.
+        assertNull(mPackageStatusStorage.getPackageStatus());
+
+        CheckToken token2 = mPackageStatusStorage.generateCheckToken(VALID_PACKAGE_VERSIONS);
+
+        // Token after a reset should still be distinct.
+        assertFalse(token1.equals(token2));
+
+        // Now clear the state again.
+        mPackageStatusStorage.resetCheckState();
+
+        // After reset, there should be no package state again.
+        assertNull(mPackageStatusStorage.getPackageStatus());
+
+        CheckToken token3 = mPackageStatusStorage.generateCheckToken(VALID_PACKAGE_VERSIONS);
+
+        // A CheckToken generated after a reset should still be distinct.
+        assertFalse(token2.equals(token3));
+    }
+
+    @Test
+    public void generateCheckToken_missingRowBehavior() {
+        // Assert initial state.
+        assertNull(mPackageStatusStorage.getPackageStatus());
+
+        CheckToken token1 = mPackageStatusStorage.generateCheckToken(VALID_PACKAGE_VERSIONS);
+        assertNotNull(token1);
+
+        // There should now be state.
+        assertNotNull(mPackageStatusStorage.getPackageStatus());
+
+        // Corrupt the table by removing the one row.
+        mPackageStatusStorage.deleteRowForTests();
+
+        // Check that generateCheckToken recovers.
+        assertNotNull(mPackageStatusStorage.generateCheckToken(VALID_PACKAGE_VERSIONS));
+    }
+
+    @Test
+    public void getPackageStatus_missingRowBehavior() {
+        // Assert initial state.
+        assertNull(mPackageStatusStorage.getPackageStatus());
+
+        CheckToken token1 = mPackageStatusStorage.generateCheckToken(VALID_PACKAGE_VERSIONS);
+        assertNotNull(token1);
+
+        // There should now be a state.
+        assertNotNull(mPackageStatusStorage.getPackageStatus());
+
+        // Corrupt the table by removing the one row.
+        mPackageStatusStorage.deleteRowForTests();
+
+        assertNull(mPackageStatusStorage.getPackageStatus());
+    }
+
+    @Test
+    public void markChecked_missingRowBehavior() {
+        // Assert initial state.
+        CheckToken token1 = mPackageStatusStorage.generateCheckToken(VALID_PACKAGE_VERSIONS);
+        assertNotNull(token1);
+
+        // There should now be a state.
+        assertNotNull(mPackageStatusStorage.getPackageStatus());
+
+        // Corrupt the table by removing the one row.
+        mPackageStatusStorage.deleteRowForTests();
+
+        // The missing row should mean token1 is now considered invalid, so we should get a false.
+        assertFalse(mPackageStatusStorage.markChecked(token1, true /* succeeded */));
+
+        // The storage should have recovered and we should be able to carry on like before.
+        CheckToken token2 = mPackageStatusStorage.generateCheckToken(VALID_PACKAGE_VERSIONS);
+        assertTrue(mPackageStatusStorage.markChecked(token2, true /* succeeded */));
+    }
+
+    @Test
+    public void checkToken_tokenIsUnique() {
+        PackageVersions packageVersions = VALID_PACKAGE_VERSIONS;
+        PackageStatus expectedPackageStatus =
+                new PackageStatus(PackageStatus.CHECK_STARTED, packageVersions);
+
+        CheckToken token1 = mPackageStatusStorage.generateCheckToken(packageVersions);
+        assertEquals(packageVersions, token1.mPackageVersions);
+
+        PackageStatus actualPackageStatus1 = mPackageStatusStorage.getPackageStatus();
+        assertEquals(expectedPackageStatus, actualPackageStatus1);
+
+        CheckToken token2 = mPackageStatusStorage.generateCheckToken(packageVersions);
+        assertEquals(packageVersions, token1.mPackageVersions);
+        assertFalse(token1.mOptimisticLockId == token2.mOptimisticLockId);
+        assertFalse(token1.equals(token2));
+    }
+
+    @Test
+    public void markChecked_checkSucceeded() {
+        PackageVersions packageVersions = VALID_PACKAGE_VERSIONS;
+
+        CheckToken token = mPackageStatusStorage.generateCheckToken(packageVersions);
+        boolean writeOk = mPackageStatusStorage.markChecked(token, true /* succeeded */);
+        assertTrue(writeOk);
+
+        PackageStatus expectedPackageStatus =
+                new PackageStatus(PackageStatus.CHECK_COMPLETED_SUCCESS, packageVersions);
+        assertEquals(expectedPackageStatus, mPackageStatusStorage.getPackageStatus());
+    }
+
+    @Test
+    public void markChecked_checkFailed() {
+        PackageVersions packageVersions = VALID_PACKAGE_VERSIONS;
+
+        CheckToken token = mPackageStatusStorage.generateCheckToken(packageVersions);
+        boolean writeOk = mPackageStatusStorage.markChecked(token, false /* succeeded */);
+        assertTrue(writeOk);
+
+        PackageStatus expectedPackageStatus =
+                new PackageStatus(PackageStatus.CHECK_COMPLETED_FAILURE, packageVersions);
+        assertEquals(expectedPackageStatus, mPackageStatusStorage.getPackageStatus());
+    }
+
+    @Test
+    public void markChecked_optimisticLocking_multipleToken() {
+        PackageVersions packageVersions = VALID_PACKAGE_VERSIONS;
+        CheckToken token1 = mPackageStatusStorage.generateCheckToken(packageVersions);
+        CheckToken token2 = mPackageStatusStorage.generateCheckToken(packageVersions);
+
+        PackageStatus packageStatusBeforeChecked = mPackageStatusStorage.getPackageStatus();
+
+        boolean writeOk1 = mPackageStatusStorage.markChecked(token1, true /* succeeded */);
+        // Generation of token2 should mean that token1 is no longer valid.
+        assertFalse(writeOk1);
+        assertEquals(packageStatusBeforeChecked, mPackageStatusStorage.getPackageStatus());
+
+        boolean writeOk2 = mPackageStatusStorage.markChecked(token2, true /* succeeded */);
+        // token2 should still be valid, and the attempt with token1 should have had no effect.
+        assertTrue(writeOk2);
+        PackageStatus expectedPackageStatus =
+                new PackageStatus(PackageStatus.CHECK_COMPLETED_SUCCESS, packageVersions);
+        assertEquals(expectedPackageStatus, mPackageStatusStorage.getPackageStatus());
+    }
+
+    @Test
+    public void markChecked_optimisticLocking_repeatedTokenUse() {
+        PackageVersions packageVersions = VALID_PACKAGE_VERSIONS;
+        CheckToken token = mPackageStatusStorage.generateCheckToken(packageVersions);
+
+        boolean writeOk1 = mPackageStatusStorage.markChecked(token, true /* succeeded */);
+        assertTrue(writeOk1);
+
+        PackageStatus expectedPackageStatus =
+                new PackageStatus(PackageStatus.CHECK_COMPLETED_SUCCESS, packageVersions);
+        assertEquals(expectedPackageStatus, mPackageStatusStorage.getPackageStatus());
+
+        // token cannot be reused.
+        boolean writeOk2 = mPackageStatusStorage.markChecked(token, true /* succeeded */);
+        assertFalse(writeOk2);
+        assertEquals(expectedPackageStatus, mPackageStatusStorage.getPackageStatus());
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/timezone/PackageStatusTest.java b/services/tests/servicestests/src/com/android/server/timezone/PackageStatusTest.java
new file mode 100644
index 0000000..c0ae81e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/timezone/PackageStatusTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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 org.junit.Test;
+
+import android.support.test.filters.SmallTest;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+@SmallTest
+public class PackageStatusTest {
+
+    @Test
+    public void equals() {
+        PackageVersions packageVersions1 =
+                new PackageVersions(1 /* updateAppVersion */, 1 /* dataAppVersion */);
+        PackageVersions packageVersions2 =
+                new PackageVersions(2 /* updateAppVersion */, 1 /* dataAppVersion */);
+        assertFalse(packageVersions1.equals(packageVersions2));
+
+        PackageStatus baseline =
+                new PackageStatus(PackageStatus.CHECK_STARTED, packageVersions1);
+        assertEquals(baseline, baseline);
+
+        PackageStatus deepEqual =
+                new PackageStatus(PackageStatus.CHECK_STARTED, packageVersions1);
+        assertEquals(baseline, deepEqual);
+
+        PackageStatus differentStatus =
+                new PackageStatus(PackageStatus.CHECK_COMPLETED_SUCCESS, packageVersions1);
+        assertFalse(differentStatus.equals(baseline));
+
+        PackageStatus differentPackageVersions =
+                new PackageStatus(PackageStatus.CHECK_STARTED, packageVersions2);
+        assertFalse(differentPackageVersions.equals(baseline));
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/timezone/PackageTrackerTest.java b/services/tests/servicestests/src/com/android/server/timezone/PackageTrackerTest.java
new file mode 100644
index 0000000..45b0af3
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/timezone/PackageTrackerTest.java
@@ -0,0 +1,1471 @@
+/*
+ * 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 org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import android.app.timezone.RulesUpdaterContract;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.TimeZoneRulesDataContract;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.hamcrest.MockitoHamcrest.argThat;
+
+/**
+ * White box interaction / unit testing of the {@link PackageTracker}.
+ */
+@SmallTest
+public class PackageTrackerTest {
+    private static final String UPDATE_APP_PACKAGE_NAME = "updateAppPackageName";
+    private static final String DATA_APP_PACKAGE_NAME = "dataAppPackageName";
+    private static final PackageVersions INITIAL_APP_PACKAGE_VERSIONS =
+            new PackageVersions(2 /* updateAppVersion */, 2 /* dataAppVersion */);
+
+    private ConfigHelper mMockConfigHelper;
+    private PackageManagerHelper mMockPackageManagerHelper;
+
+    private FakeClockHelper mFakeClock;
+    private FakeIntentHelper mFakeIntentHelper;
+    private PackageStatusStorage mPackageStatusStorage;
+    private PackageTracker mPackageTracker;
+
+    @Before
+    public void setUp() throws Exception {
+        Context context = InstrumentationRegistry.getContext();
+
+        mFakeClock = new FakeClockHelper();
+
+        // Read-only interfaces so are easy to mock.
+        mMockConfigHelper = mock(ConfigHelper.class);
+        mMockPackageManagerHelper = mock(PackageManagerHelper.class);
+
+        // Using the instrumentation context means the database is created in a test app-specific
+        // directory. We can use the real thing for this test.
+        mPackageStatusStorage = new PackageStatusStorage(context);
+
+        // For other interactions with the Android framework we create a fake object.
+        mFakeIntentHelper = new FakeIntentHelper();
+
+        // Create the PackageTracker to use in tests.
+        mPackageTracker = new PackageTracker(
+                mFakeClock,
+                mMockConfigHelper,
+                mMockPackageManagerHelper,
+                mPackageStatusStorage,
+                mFakeIntentHelper);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mPackageStatusStorage != null) {
+            mPackageStatusStorage.deleteDatabaseForTests();
+        }
+    }
+
+    @Test
+    public void trackingDisabled_intentHelperNotUsed() {
+        // Set up device configuration.
+        configureTrackingDisabled();
+
+        // Initialize the tracker.
+        mPackageTracker.start();
+
+        // Check the IntentHelper was not initialized.
+        mFakeIntentHelper.assertNotInitialized();
+
+        // Check reliability triggering state.
+        mFakeIntentHelper.assertReliabilityTriggeringDisabled();
+    }
+
+    @Test
+    public void trackingDisabled_triggerUpdateIfNeededNotAllowed() {
+        // Set up device configuration.
+        configureTrackingDisabled();
+
+        // Initialize the tracker.
+        mPackageTracker.start();
+
+        // Check reliability triggering state.
+        mFakeIntentHelper.assertReliabilityTriggeringDisabled();
+
+        try {
+            // This call should also not be allowed and will throw an exception if tracking is
+            // disabled.
+            mPackageTracker.triggerUpdateIfNeeded(true);
+            fail();
+        } catch (IllegalStateException expected) {}
+
+        // Check reliability triggering state.
+        mFakeIntentHelper.assertReliabilityTriggeringDisabled();
+    }
+
+    @Test
+    public void trackingDisabled_unsolicitedResultsIgnored_withoutToken() {
+        // Set up device configuration.
+        configureTrackingDisabled();
+
+        // Initialize the tracker.
+        mPackageTracker.start();
+
+        // Check reliability triggering state.
+        mFakeIntentHelper.assertReliabilityTriggeringDisabled();
+
+        // Receiving a check result when tracking is disabled should cause the storage to be
+        // reset.
+        mPackageTracker.recordCheckResult(null /* checkToken */, true /* success */);
+
+        // Check reliability triggering state.
+        mFakeIntentHelper.assertReliabilityTriggeringDisabled();
+
+        // Assert the storage was reset.
+        checkPackageStorageStatusIsInitialOrReset();
+    }
+
+    @Test
+    public void trackingDisabled_unsolicitedResultsIgnored_withToken() {
+        // Set up device configuration.
+        configureTrackingDisabled();
+
+        // Set the storage into an arbitrary state so we can detect a reset.
+        mPackageStatusStorage.generateCheckToken(INITIAL_APP_PACKAGE_VERSIONS);
+
+        // Initialize the tracker.
+        mPackageTracker.start();
+
+        // Check reliability triggering state.
+        mFakeIntentHelper.assertReliabilityTriggeringDisabled();
+
+        // Receiving a check result when tracking is disabled should cause the storage to be reset.
+        mPackageTracker.recordCheckResult(createArbitraryCheckToken(), true /* success */);
+
+        // Check reliability triggering state.
+        mFakeIntentHelper.assertReliabilityTriggeringDisabled();
+
+        // Assert the storage was reset.
+        checkPackageStorageStatusIsInitialOrReset();
+    }
+
+    @Test
+    public void trackingEnabled_updateAppConfigMissing() throws Exception {
+        // Set up device configuration.
+        configureTrackingEnabled();
+        configureReliabilityConfigSettingsOk();
+        configureUpdateAppPackageNameMissing();
+        configureDataAppPackageOk(DATA_APP_PACKAGE_NAME);
+
+        try {
+            // Initialize the tracker.
+            mPackageTracker.start();
+            fail();
+        } catch (RuntimeException expected) {}
+
+        mFakeIntentHelper.assertNotInitialized();
+
+        // Check reliability triggering state.
+        mFakeIntentHelper.assertReliabilityTriggeringDisabled();
+    }
+
+    // TODO(nfuller): Uncomment or delete when it's clear what will happen with http://b/35995024
+    // @Test
+    // public void trackingEnabled_updateAppNotPrivileged() throws Exception {
+    //     // Set up device configuration.
+    //     configureTrackingEnabled();
+    //     configureReliabilityConfigSettingsOk();
+    //     configureUpdateAppPackageNotPrivileged(UPDATE_APP_PACKAGE_NAME);
+    //     configureDataAppPackageOk(DATA_APP_PACKAGE_NAME);
+    //
+    //     try {
+    //         // Initialize the tracker.
+    //         mPackageTracker.start();
+    //         fail();
+    //     } catch (RuntimeException expected) {}
+    //
+    //     mFakeIntentHelper.assertNotInitialized();
+    //
+    //     // Check reliability triggering state.
+    //     mFakeIntentHelper.assertReliabilityTriggeringDisabled();
+    // }
+
+    @Test
+    public void trackingEnabled_dataAppConfigMissing() throws Exception {
+        // Set up device configuration.
+        configureTrackingEnabled();
+        configureReliabilityConfigSettingsOk();
+        configureUpdateAppPackageOk(UPDATE_APP_PACKAGE_NAME);
+        configureDataAppPackageNameMissing();
+
+        try {
+            // Initialize the tracker.
+            mPackageTracker.start();
+            fail();
+        } catch (RuntimeException expected) {}
+
+        mFakeIntentHelper.assertNotInitialized();
+
+        // Check reliability triggering state.
+        mFakeIntentHelper.assertReliabilityTriggeringDisabled();
+    }
+
+    // TODO(nfuller): Uncomment or delete when it's clear what will happen with http://b/35995024
+    // @Test
+    // public void trackingEnabled_dataAppNotPrivileged() throws Exception {
+    //     // Set up device configuration.
+    //     configureTrackingEnabled();
+    //     configureReliabilityConfigSettingsOk();
+    //     configureUpdateAppPackageOk(UPDATE_APP_PACKAGE_NAME);
+    //     configureDataAppPackageNotPrivileged(DATA_APP_PACKAGE_NAME);
+    //
+    //     try {
+    //         // Initialize the tracker.
+    //         mPackageTracker.start();
+    //         fail();
+    //     } catch (RuntimeException expected) {}
+    //
+    //     mFakeIntentHelper.assertNotInitialized();
+    //
+    //     // Check reliability triggering state.
+    //     mFakeIntentHelper.assertReliabilityTriggeringDisabled();
+    // }
+
+    @Test
+    public void trackingEnabled_packageUpdate_badUpdateAppManifestEntry() throws Exception {
+        // Set up device configuration.
+        configureTrackingEnabled();
+        configureReliabilityConfigSettingsOk();
+        configureValidApplications();
+
+        // Initialize the tracker.
+        mPackageTracker.start();
+
+        // Check the intent helper is properly configured.
+        checkIntentHelperInitializedAndReliabilityTrackingEnabled();
+
+        // Check the initial storage state.
+        checkPackageStorageStatusIsInitialOrReset();
+
+        // Configure a bad manifest for the update app. Should effectively turn off tracking.
+        PackageVersions packageVersions =
+                new PackageVersions(2 /* updateAppPackageVersion */, 3 /* dataAppPackageVersion */);
+        configureUpdateAppManifestBad(UPDATE_APP_PACKAGE_NAME);
+        configureDataAppManifestOk(DATA_APP_PACKAGE_NAME);
+        configureUpdateAppPackageVersion(
+                UPDATE_APP_PACKAGE_NAME, packageVersions.mUpdateAppVersion);
+        configureDataAppPackageVersion(DATA_APP_PACKAGE_NAME, packageVersions.mDataAppVersion);
+        // Simulate a tracked package being updated.
+        mFakeIntentHelper.simulatePackageUpdatedEvent();
+
+        // Assert the PackageTracker did not attempt to trigger an update.
+        mFakeIntentHelper.assertUpdateNotTriggered();
+
+        // Check reliability triggering state.
+        mFakeIntentHelper.assertReliabilityTriggeringDisabled();
+
+        // Assert the storage was not touched.
+        checkPackageStorageStatusIsInitialOrReset();
+    }
+
+    @Test
+    public void trackingEnabled_packageUpdate_badDataAppManifestEntry() throws Exception {
+        // Set up device configuration.
+        configureTrackingEnabled();
+        configureReliabilityConfigSettingsOk();
+        configureValidApplications();
+
+        // Initialize the tracker.
+        mPackageTracker.start();
+
+        // Check the intent helper is properly configured.
+        checkIntentHelperInitializedAndReliabilityTrackingEnabled();
+
+        // Check the initial storage state.
+        checkPackageStorageStatusIsInitialOrReset();
+
+        // Configure a bad manifest for the data app. Should effectively turn off tracking.
+        PackageVersions packageVersions =
+                new PackageVersions(2 /* updateAppPackageVersion */, 3 /* dataAppPackageVersion */);
+        configureUpdateAppManifestOk(UPDATE_APP_PACKAGE_NAME);
+        configureDataAppManifestBad(DATA_APP_PACKAGE_NAME);
+        configureUpdateAppPackageVersion(
+                UPDATE_APP_PACKAGE_NAME, packageVersions.mUpdateAppVersion);
+        configureDataAppPackageVersion(DATA_APP_PACKAGE_NAME, packageVersions.mDataAppVersion);
+        mFakeIntentHelper.simulatePackageUpdatedEvent();
+
+        // Assert the PackageTracker did not attempt to trigger an update.
+        mFakeIntentHelper.assertUpdateNotTriggered();
+
+        // Check reliability triggering state.
+        mFakeIntentHelper.assertReliabilityTriggeringDisabled();
+
+        // Assert the storage was not touched.
+        checkPackageStorageStatusIsInitialOrReset();
+    }
+
+    @Test
+    public void trackingEnabled_packageUpdate_responseWithToken_success() throws Exception {
+        trackingEnabled_packageUpdate_responseWithToken(true);
+    }
+
+    @Test
+    public void trackingEnabled_packageUpdate_responseWithToken_failed() throws Exception {
+        trackingEnabled_packageUpdate_responseWithToken(false);
+    }
+
+    private void trackingEnabled_packageUpdate_responseWithToken(boolean success) throws Exception {
+        // Set up device configuration.
+        configureTrackingEnabled();
+        configureReliabilityConfigSettingsOk();
+        configureValidApplications();
+
+        // Initialize the tracker.
+        mPackageTracker.start();
+
+        // Check the intent helper is properly configured.
+        checkIntentHelperInitializedAndReliabilityTrackingEnabled();
+
+        // Check the initial storage state.
+        checkPackageStorageStatusIsInitialOrReset();
+
+        // Simulate a tracked package being updated.
+        PackageVersions packageVersions =
+                new PackageVersions(2 /* updateAppPackageVersion */, 3 /* dataAppPackageVersion */);
+        simulatePackageInstallation(packageVersions);
+
+        // Confirm an update was triggered.
+        checkUpdateCheckTriggered(packageVersions);
+
+        // Get the token that was passed to the intent helper, and pass it back.
+        CheckToken token = mFakeIntentHelper.captureAndResetLastToken();
+        mPackageTracker.recordCheckResult(token, success);
+
+        // Check storage and reliability triggering state.
+        if (success) {
+            checkUpdateCheckSuccessful(packageVersions);
+        } else {
+            checkUpdateCheckFailed(packageVersions);
+        }
+    }
+
+    @Test
+    public void trackingEnabled_packageUpdate_responseWithoutTokenCausesStorageReset_success()
+            throws Exception {
+        trackingEnabled_packageUpdate_responseWithoutTokenCausesStorageReset(true);
+    }
+
+    @Test
+    public void trackingEnabled_packageUpdate_responseWithoutTokenCausesStorageReset_failed()
+            throws Exception {
+        trackingEnabled_packageUpdate_responseWithoutTokenCausesStorageReset(false);
+    }
+
+    private void trackingEnabled_packageUpdate_responseWithoutTokenCausesStorageReset(
+            boolean success) throws Exception {
+        // Set up device configuration.
+        configureTrackingEnabled();
+        configureReliabilityConfigSettingsOk();
+        configureValidApplications();
+
+        // Initialize the tracker.
+        mPackageTracker.start();
+
+        // Check the intent helper is properly configured.
+        checkIntentHelperInitializedAndReliabilityTrackingEnabled();
+
+        // Check the initial storage state.
+        checkPackageStorageStatusIsInitialOrReset();
+
+        // Set up installed app versions / manifests.
+        PackageVersions packageVersions =
+                new PackageVersions(2 /* updateAppPackageVersion */, 3 /* dataAppPackageVersion */);
+        simulatePackageInstallation(packageVersions);
+
+        // Confirm an update was triggered.
+        checkUpdateCheckTriggered(packageVersions);
+
+        // Ignore the token that was given to the intent helper, just pass null.
+        mPackageTracker.recordCheckResult(null /* checkToken */, success);
+
+        // Check reliability triggering state.
+        mFakeIntentHelper.assertReliabilityTriggeringEnabled();
+
+        // Assert the storage was reset.
+        checkPackageStorageStatusIsInitialOrReset();
+    }
+
+    /**
+     * Two package updates triggered for the same package versions. The second is triggered while
+     * the first is still happening.
+     */
+    @Test
+    public void trackingEnabled_packageUpdate_twoChecksNoPackageChange_secondWhileFirstInProgress()
+            throws Exception {
+        // Set up device configuration.
+        configureTrackingEnabled();
+        configureReliabilityConfigSettingsOk();
+        configureValidApplications();
+
+        // Initialize the tracker.
+        mPackageTracker.start();
+
+        // Check the intent helper is properly configured.
+        checkIntentHelperInitializedAndReliabilityTrackingEnabled();
+
+        // Check the initial storage state.
+        checkPackageStorageStatusIsInitialOrReset();
+
+        // Simulate package installation.
+        PackageVersions packageVersions =
+                new PackageVersions(2 /* updateAppPackageVersion */, 3 /* dataAppPackageVersion */);
+        simulatePackageInstallation(packageVersions);
+
+        // Confirm an update was triggered.
+        checkUpdateCheckTriggered(packageVersions);
+
+        // Get the first token.
+        CheckToken token1 = mFakeIntentHelper.captureAndResetLastToken();
+        assertEquals(packageVersions, token1.mPackageVersions);
+
+        // Now attempt to generate another check while the first is in progress and without having
+        // updated the package versions. The PackageTracker should trigger again for safety.
+        simulatePackageInstallation(packageVersions);
+
+        // Confirm an update was triggered.
+        checkUpdateCheckTriggered(packageVersions);
+
+        CheckToken token2 = mFakeIntentHelper.captureAndResetLastToken();
+        assertEquals(packageVersions, token2.mPackageVersions);
+        assertEquals(token1.mPackageVersions, token2.mPackageVersions);
+        assertTrue(token1.mOptimisticLockId != token2.mOptimisticLockId);
+    }
+
+    /**
+     * Two package updates triggered for the same package versions. The second happens after
+     * the first has succeeded.
+     */
+    @Test
+    public void trackingEnabled_packageUpdate_twoChecksNoPackageChange_sequential()
+            throws Exception {
+        // Set up device configuration.
+        configureTrackingEnabled();
+        configureReliabilityConfigSettingsOk();
+        configureValidApplications();
+
+        // Initialize the tracker.
+        mPackageTracker.start();
+
+        // Check the intent helper is properly configured.
+        checkIntentHelperInitializedAndReliabilityTrackingEnabled();
+
+        // Check the initial storage state.
+        checkPackageStorageStatusIsInitialOrReset();
+
+        // Simulate package installation.
+        PackageVersions packageVersions =
+                new PackageVersions(2 /* updateAppPackageVersion */, 3 /* dataAppPackageVersion */);
+        simulatePackageInstallation(packageVersions);
+
+        // Confirm an update was triggered.
+        checkUpdateCheckTriggered(packageVersions);
+
+        // Get the token.
+        CheckToken token = mFakeIntentHelper.captureAndResetLastToken();
+        assertEquals(packageVersions, token.mPackageVersions);
+
+        // Simulate a successful check.
+        mPackageTracker.recordCheckResult(token, true /* success */);
+
+        // Check storage and reliability triggering state.
+        checkUpdateCheckSuccessful(packageVersions);
+
+        // Now attempt to generate another check, but without having updated the package. The
+        // PackageTracker should be smart enough to recognize there's nothing to do here.
+        simulatePackageInstallation(packageVersions);
+
+        // Assert the PackageTracker did not attempt to trigger an update.
+        mFakeIntentHelper.assertUpdateNotTriggered();
+
+        // Check storage and reliability triggering state.
+        checkUpdateCheckSuccessful(packageVersions);
+    }
+
+    /**
+     * Two package updates triggered for the same package versions. The second is triggered after
+     * the first has failed.
+     */
+    @Test
+    public void trackingEnabled_packageUpdate_afterFailure() throws Exception {
+        // Set up device configuration.
+        configureTrackingEnabled();
+        configureReliabilityConfigSettingsOk();
+        configureValidApplications();
+
+        // Initialize the tracker.
+        mPackageTracker.start();
+
+        // Check the intent helper is properly configured.
+        checkIntentHelperInitializedAndReliabilityTrackingEnabled();
+
+        // Check the initial storage state.
+        checkPackageStorageStatusIsInitialOrReset();
+
+        // Simulate package installation.
+        PackageVersions packageVersions =
+                new PackageVersions(2 /* updateAppPackageVersion */, 3 /* dataAppPackageVersion */);
+        simulatePackageInstallation(packageVersions);
+
+        // Confirm an update was triggered.
+        checkUpdateCheckTriggered(packageVersions);
+
+        // Get the first token.
+        CheckToken token1 = mFakeIntentHelper.captureAndResetLastToken();
+        assertEquals(packageVersions, token1.mPackageVersions);
+
+        // Simulate an *unsuccessful* check.
+        mPackageTracker.recordCheckResult(token1, false /* success */);
+
+        // Check storage and reliability triggering state.
+        checkUpdateCheckFailed(packageVersions);
+
+        // Now generate another check, but without having updated the package. The
+        // PackageTracker should recognize the last check failed and trigger again.
+        simulatePackageInstallation(packageVersions);
+
+        // Confirm an update was triggered.
+        checkUpdateCheckTriggered(packageVersions);
+
+        // Get the second token.
+        CheckToken token2 = mFakeIntentHelper.captureAndResetLastToken();
+
+        // Assert some things about the tokens.
+        assertEquals(packageVersions, token2.mPackageVersions);
+        assertTrue(token1.mOptimisticLockId != token2.mOptimisticLockId);
+
+        // For completeness, now simulate this check was successful.
+        mPackageTracker.recordCheckResult(token2, true /* success */);
+
+        // Check storage and reliability triggering state.
+        checkUpdateCheckSuccessful(packageVersions);
+    }
+
+    /**
+     * Two package updates triggered for different package versions. The second is triggered while
+     * the first is still happening.
+     */
+    @Test
+    public void trackingEnabled_packageUpdate_twoChecksWithPackageChange_firstCheckInProcess()
+            throws Exception {
+        // Set up device configuration.
+        configureTrackingEnabled();
+        configureReliabilityConfigSettingsOk();
+        configureValidApplications();
+
+        // Initialize the package tracker.
+        mPackageTracker.start();
+
+        // Check the intent helper is properly configured.
+        checkIntentHelperInitializedAndReliabilityTrackingEnabled();
+
+        // Check the initial storage state.
+        checkPackageStorageStatusIsInitialOrReset();
+
+        // Simulate package installation.
+        PackageVersions packageVersions1 =
+                new PackageVersions(2 /* updateAppPackageVersion */, 3 /* dataAppPackageVersion */);
+        simulatePackageInstallation(packageVersions1);
+
+        // Confirm an update was triggered.
+        checkUpdateCheckTriggered(packageVersions1);
+
+        // Get the first token.
+        CheckToken token1 = mFakeIntentHelper.captureAndResetLastToken();
+        assertEquals(packageVersions1, token1.mPackageVersions);
+
+        // Simulate a tracked package being updated a second time (before the response for the
+        // first has been received).
+        PackageVersions packageVersions2 =
+                new PackageVersions(3 /* updateAppPackageVersion */, 4 /* dataAppPackageVersion */);
+        simulatePackageInstallation(packageVersions2);
+
+        // Confirm an update was triggered.
+        checkUpdateCheckTriggered(packageVersions2);
+
+        // Get the second token.
+        CheckToken token2 = mFakeIntentHelper.captureAndResetLastToken();
+        assertEquals(packageVersions2, token2.mPackageVersions);
+
+        // token1 should be invalid because the token2 was generated.
+        mPackageTracker.recordCheckResult(token1, true /* success */);
+
+        // Reliability triggering should still be enabled.
+        mFakeIntentHelper.assertReliabilityTriggeringEnabled();
+
+        // Check the expected storage state.
+        checkPackageStorageStatus(PackageStatus.CHECK_STARTED, packageVersions2);
+
+        // token2 should still be accepted.
+        mPackageTracker.recordCheckResult(token2, true /* success */);
+
+        // Check storage and reliability triggering state.
+        checkUpdateCheckSuccessful(packageVersions2);
+    }
+
+    /**
+     * Two package updates triggered for different package versions. The second is triggered after
+     * the first has completed successfully.
+     */
+    @Test
+    public void trackingEnabled_packageUpdate_twoChecksWithPackageChange_sequential()
+            throws Exception {
+        // Set up device configuration.
+        configureTrackingEnabled();
+        configureReliabilityConfigSettingsOk();
+        configureValidApplications();
+
+        // Initialize the package tracker.
+        mPackageTracker.start();
+
+        // Check the intent helper is properly configured.
+        checkIntentHelperInitializedAndReliabilityTrackingEnabled();
+
+        // Check the initial storage state.
+        checkPackageStorageStatusIsInitialOrReset();
+
+        // Simulate package installation.
+        PackageVersions packageVersions1 =
+                new PackageVersions(2 /* updateAppPackageVersion */, 3 /* dataAppPackageVersion */);
+        simulatePackageInstallation(packageVersions1);
+
+        // Confirm an update was triggered.
+        checkUpdateCheckTriggered(packageVersions1);
+
+        // Get the first token.
+        CheckToken token1 = mFakeIntentHelper.captureAndResetLastToken();
+        assertEquals(packageVersions1, token1.mPackageVersions);
+
+        // token1 should be accepted.
+        mPackageTracker.recordCheckResult(token1, true /* success */);
+
+        // Check storage and reliability triggering state.
+        checkUpdateCheckSuccessful(packageVersions1);
+
+        // Simulate a tracked package being updated a second time.
+        PackageVersions packageVersions2 =
+                new PackageVersions(3 /* updateAppPackageVersion */, 4 /* dataAppPackageVersion */);
+        simulatePackageInstallation(packageVersions2);
+
+        // Confirm an update was triggered.
+        checkUpdateCheckTriggered(packageVersions2);
+
+        // Get the second token.
+        CheckToken token2 = mFakeIntentHelper.captureAndResetLastToken();
+        assertEquals(packageVersions2, token2.mPackageVersions);
+
+        // token2 should still be accepted.
+        mPackageTracker.recordCheckResult(token2, true /* success */);
+
+        // Check storage and reliability triggering state.
+        checkUpdateCheckSuccessful(packageVersions2);
+    }
+
+    /**
+     * Replaying the same token twice.
+     */
+    @Test
+    public void trackingEnabled_packageUpdate_sameTokenReplayFails() throws Exception {
+        // Set up device configuration.
+        configureTrackingEnabled();
+        configureReliabilityConfigSettingsOk();
+        configureValidApplications();
+
+        // Initialize the package tracker.
+        mPackageTracker.start();
+
+        // Check the intent helper is properly configured.
+        checkIntentHelperInitializedAndReliabilityTrackingEnabled();
+
+        // Check the initial storage state.
+        checkPackageStorageStatusIsInitialOrReset();
+
+        // Simulate package installation.
+        PackageVersions packageVersions1 =
+                new PackageVersions(2 /* updateAppPackageVersion */, 3 /* dataAppPackageVersion */);
+        simulatePackageInstallation(packageVersions1);
+
+        // Confirm an update was triggered.
+        checkUpdateCheckTriggered(packageVersions1);
+
+        // Get the first token.
+        CheckToken token1 = mFakeIntentHelper.captureAndResetLastToken();
+        assertEquals(packageVersions1, token1.mPackageVersions);
+
+        // token1 should be accepted.
+        mPackageTracker.recordCheckResult(token1, true /* success */);
+
+        // Check storage and reliability triggering state.
+        checkUpdateCheckSuccessful(packageVersions1);
+
+        // Apply token1 again.
+        mPackageTracker.recordCheckResult(token1, true /* success */);
+
+        // Check the expected storage state. No real way to tell if it has been updated, but
+        // we can check the final state is still what it should be.
+        checkPackageStorageStatus(PackageStatus.CHECK_COMPLETED_SUCCESS, packageVersions1);
+
+        // Under the covers we expect it to fail to update because the storage should recognize that
+        // the token is no longer valid.
+        mFakeIntentHelper.assertReliabilityTriggeringEnabled();
+
+        // Peek inside the package tracker to make sure it is tracking failure counts properly.
+        assertEquals(1, mPackageTracker.getCheckFailureCountForTests());
+    }
+
+    @Test
+    public void trackingEnabled_reliabilityTrigger_firstTime_initialStorage() throws Exception {
+        // Set up device configuration.
+        configureTrackingEnabled();
+        configureReliabilityConfigSettingsOk();
+        PackageVersions packageVersions = configureValidApplications();
+
+        // Initialize the package tracker.
+        mPackageTracker.start();
+
+        // Check the intent helper is properly configured.
+        checkIntentHelperInitializedAndReliabilityTrackingEnabled();
+
+        // Check the initial storage state.
+        checkPackageStorageStatusIsInitialOrReset();
+
+        // Simulate a reliability trigger.
+        mFakeIntentHelper.simulateReliabilityTrigger();
+
+        // Assert the PackageTracker did trigger an update.
+        checkUpdateCheckTriggered(packageVersions);
+
+        // Confirm the token was correct.
+        CheckToken token1 = mFakeIntentHelper.captureAndResetLastToken();
+        assertEquals(packageVersions, token1.mPackageVersions);
+
+        // token1 should be accepted.
+        mPackageTracker.recordCheckResult(token1, true /* success */);
+
+        // Check storage and reliability triggering state.
+        checkUpdateCheckSuccessful(packageVersions);
+    }
+
+    @Test
+    public void trackingEnabled_reliabilityTrigger_afterRebootNoTriggerNeeded() throws Exception {
+        // Set up device configuration.
+        configureTrackingEnabled();
+        configureReliabilityConfigSettingsOk();
+        PackageVersions packageVersions = configureValidApplications();
+
+        // Force the storage into a state we want.
+        mPackageStatusStorage.forceCheckStateForTests(
+                PackageStatus.CHECK_COMPLETED_SUCCESS, packageVersions);
+
+        // Initialize the package tracker.
+        mPackageTracker.start();
+
+        // Check the intent helper is properly configured.
+        checkIntentHelperInitializedAndReliabilityTrackingEnabled();
+
+        // Check the initial storage state.
+        checkPackageStorageStatus(PackageStatus.CHECK_COMPLETED_SUCCESS, packageVersions);
+
+        // Simulate a reliability trigger.
+        mFakeIntentHelper.simulateReliabilityTrigger();
+
+        // Assert the PackageTracker did not attempt to trigger an update.
+        mFakeIntentHelper.assertUpdateNotTriggered();
+
+        // Check storage and reliability triggering state.
+        checkUpdateCheckSuccessful(packageVersions);
+    }
+
+    /**
+     * Simulates the device starting where the storage records do not match the installed app
+     * versions. The reliability trigger should cause the package tracker to perform a check.
+     */
+    @Test
+    public void trackingEnabled_reliabilityTrigger_afterRebootTriggerNeededBecausePreviousFailed()
+            throws Exception {
+        // Set up device configuration.
+        configureTrackingEnabled();
+        configureReliabilityConfigSettingsOk();
+
+        PackageVersions oldPackageVersions = new PackageVersions(1, 1);
+        PackageVersions currentPackageVersions = new PackageVersions(2, 2);
+
+        // Simulate there being a newer version installed than the one recorded in storage.
+        configureValidApplications(currentPackageVersions);
+
+        // Force the storage into a state we want.
+        mPackageStatusStorage.forceCheckStateForTests(
+                PackageStatus.CHECK_COMPLETED_FAILURE, oldPackageVersions);
+
+        // Initialize the package tracker.
+        mPackageTracker.start();
+
+        // Check the intent helper is properly configured.
+        checkIntentHelperInitializedAndReliabilityTrackingEnabled();
+
+        // Check the initial storage state.
+        checkPackageStorageStatus(PackageStatus.CHECK_COMPLETED_FAILURE, oldPackageVersions);
+
+        // Simulate a reliability trigger.
+        mFakeIntentHelper.simulateReliabilityTrigger();
+
+        // Assert the PackageTracker did trigger an update.
+        checkUpdateCheckTriggered(currentPackageVersions);
+
+        // Simulate the update check completing successfully.
+        CheckToken checkToken = mFakeIntentHelper.captureAndResetLastToken();
+        mPackageTracker.recordCheckResult(checkToken, true /* success */);
+
+        // Check storage and reliability triggering state.
+        checkUpdateCheckSuccessful(currentPackageVersions);
+    }
+
+    /**
+     * Simulates persistent failures of the reliability check. It should stop after the configured
+     * number of checks.
+     */
+    @Test
+    public void trackingEnabled_reliabilityTrigger_repeatedFailures() throws Exception {
+        // Set up device configuration.
+        configureTrackingEnabled();
+
+        int retriesAllowed = 3;
+        int checkDelayMillis = 5 * 60 * 1000;
+        configureReliabilityConfigSettings(retriesAllowed, checkDelayMillis);
+
+        PackageVersions oldPackageVersions = new PackageVersions(1, 1);
+        PackageVersions currentPackageVersions = new PackageVersions(2, 2);
+
+        // Simulate there being a newer version installed than the one recorded in storage.
+        configureValidApplications(currentPackageVersions);
+
+        // Force the storage into a state we want.
+        mPackageStatusStorage.forceCheckStateForTests(
+                PackageStatus.CHECK_COMPLETED_FAILURE, oldPackageVersions);
+
+        // Initialize the package tracker.
+        mPackageTracker.start();
+
+        // Check the intent helper is properly configured.
+        checkIntentHelperInitializedAndReliabilityTrackingEnabled();
+
+        // Check the initial storage state.
+        checkPackageStorageStatus(PackageStatus.CHECK_COMPLETED_FAILURE, oldPackageVersions);
+
+        for (int i = 0; i < retriesAllowed + 1; i++) {
+            // Simulate a reliability trigger.
+            mFakeIntentHelper.simulateReliabilityTrigger();
+
+            // Assert the PackageTracker did trigger an update.
+            checkUpdateCheckTriggered(currentPackageVersions);
+
+            // Check the PackageTracker failure count before calling recordCheckResult.
+            assertEquals(i, mPackageTracker.getCheckFailureCountForTests());
+
+            // Simulate a check failure.
+            CheckToken checkToken = mFakeIntentHelper.captureAndResetLastToken();
+            mPackageTracker.recordCheckResult(checkToken, false /* success */);
+
+            // Peek inside the package tracker to make sure it is tracking failure counts properly.
+            assertEquals(i + 1, mPackageTracker.getCheckFailureCountForTests());
+
+            // Confirm nothing has changed.
+            mFakeIntentHelper.assertUpdateNotTriggered();
+            checkPackageStorageStatus(PackageStatus.CHECK_COMPLETED_FAILURE,
+                    currentPackageVersions);
+
+            // Check reliability triggering is in the correct state.
+            if (i <= retriesAllowed) {
+                mFakeIntentHelper.assertReliabilityTriggeringEnabled();
+            } else {
+                mFakeIntentHelper.assertReliabilityTriggeringDisabled();
+            }
+        }
+    }
+
+    @Test
+    public void trackingEnabled_reliabilityTrigger_failureCountIsReset() throws Exception {
+        // Set up device configuration.
+        configureTrackingEnabled();
+
+        int retriesAllowed = 3;
+        int checkDelayMillis = 5 * 60 * 1000;
+        configureReliabilityConfigSettings(retriesAllowed, checkDelayMillis);
+
+        PackageVersions oldPackageVersions = new PackageVersions(1, 1);
+        PackageVersions currentPackageVersions = new PackageVersions(2, 2);
+
+        // Simulate there being a newer version installed than the one recorded in storage.
+        configureValidApplications(currentPackageVersions);
+
+        // Force the storage into a state we want.
+        mPackageStatusStorage.forceCheckStateForTests(
+                PackageStatus.CHECK_COMPLETED_FAILURE, oldPackageVersions);
+
+        // Initialize the package tracker.
+        mPackageTracker.start();
+
+        // Check the intent helper is properly configured.
+        checkIntentHelperInitializedAndReliabilityTrackingEnabled();
+
+        // Check the initial storage state.
+        checkPackageStorageStatus(PackageStatus.CHECK_COMPLETED_FAILURE, oldPackageVersions);
+
+        // Fail (retries - 1) times.
+        for (int i = 0; i < retriesAllowed - 1; i++) {
+            // Simulate a reliability trigger.
+            mFakeIntentHelper.simulateReliabilityTrigger();
+
+            // Assert the PackageTracker did trigger an update.
+            checkUpdateCheckTriggered(currentPackageVersions);
+
+            // Check the PackageTracker failure count before calling recordCheckResult.
+            assertEquals(i, mPackageTracker.getCheckFailureCountForTests());
+
+            // Simulate a check failure.
+            CheckToken checkToken = mFakeIntentHelper.captureAndResetLastToken();
+            mPackageTracker.recordCheckResult(checkToken, false /* success */);
+
+            // Peek inside the package tracker to make sure it is tracking failure counts properly.
+            assertEquals(i + 1, mPackageTracker.getCheckFailureCountForTests());
+
+            // Confirm nothing has changed.
+            mFakeIntentHelper.assertUpdateNotTriggered();
+            checkPackageStorageStatus(PackageStatus.CHECK_COMPLETED_FAILURE,
+                    currentPackageVersions);
+
+            // Check reliability triggering is still enabled.
+            mFakeIntentHelper.assertReliabilityTriggeringEnabled();
+        }
+
+        // Simulate a reliability trigger.
+        mFakeIntentHelper.simulateReliabilityTrigger();
+
+        // Assert the PackageTracker did trigger an update.
+        checkUpdateCheckTriggered(currentPackageVersions);
+
+        // Check the PackageTracker failure count before calling recordCheckResult.
+        assertEquals(retriesAllowed - 1, mPackageTracker.getCheckFailureCountForTests());
+
+        // On the last possible try, succeed.
+        CheckToken checkToken = mFakeIntentHelper.captureAndResetLastToken();
+        mPackageTracker.recordCheckResult(checkToken, true /* success */);
+
+        checkUpdateCheckSuccessful(currentPackageVersions);
+    }
+
+    /**
+     * Simulates reliability triggers happening too close together. Package tracker should ignore
+     * the ones it doesn't need.
+     */
+    @Test
+    public void trackingEnabled_reliabilityTrigger_tooSoon() throws Exception {
+        // Set up device configuration.
+        configureTrackingEnabled();
+
+        int retriesAllowed = 5;
+        int checkDelayMillis = 5 * 60 * 1000;
+        configureReliabilityConfigSettings(retriesAllowed, checkDelayMillis);
+
+        PackageVersions oldPackageVersions = new PackageVersions(1, 1);
+        PackageVersions currentPackageVersions = new PackageVersions(2, 2);
+
+        // Simulate there being a newer version installed than the one recorded in storage.
+        configureValidApplications(currentPackageVersions);
+
+        // Force the storage into a state we want.
+        mPackageStatusStorage.forceCheckStateForTests(
+                PackageStatus.CHECK_COMPLETED_FAILURE, oldPackageVersions);
+
+        // Initialize the package tracker.
+        mPackageTracker.start();
+
+        // Check the intent helper is properly configured.
+        checkIntentHelperInitializedAndReliabilityTrackingEnabled();
+
+        // Check the initial storage state.
+        checkPackageStorageStatus(PackageStatus.CHECK_COMPLETED_FAILURE, oldPackageVersions);
+
+        // Simulate a reliability trigger.
+        mFakeIntentHelper.simulateReliabilityTrigger();
+
+        // Assert the PackageTracker did trigger an update.
+        checkUpdateCheckTriggered(currentPackageVersions);
+        CheckToken token1 = mFakeIntentHelper.captureAndResetLastToken();
+
+        // Increment the clock, but not enough.
+        mFakeClock.incrementClock(checkDelayMillis - 1);
+
+        // Simulate a reliability trigger.
+        mFakeIntentHelper.simulateReliabilityTrigger();
+
+        // Assert the PackageTracker did not trigger an update.
+        mFakeIntentHelper.assertUpdateNotTriggered();
+        checkPackageStorageStatus(PackageStatus.CHECK_STARTED, currentPackageVersions);
+        mFakeIntentHelper.assertReliabilityTriggeringEnabled();
+
+        // Increment the clock slightly more. Should now consider the response overdue.
+        mFakeClock.incrementClock(2);
+
+        // Simulate a reliability trigger.
+        mFakeIntentHelper.simulateReliabilityTrigger();
+
+        // Triggering should have happened.
+        checkUpdateCheckTriggered(currentPackageVersions);
+        CheckToken token2 = mFakeIntentHelper.captureAndResetLastToken();
+
+        // Check a new token was generated.
+        assertFalse(token1.equals(token2));
+    }
+
+    /**
+     * Tests what happens when a package update doesn't complete and a reliability trigger cleans
+     * up for it.
+     */
+    @Test
+    public void trackingEnabled_reliabilityTrigger_afterPackageUpdateDidNotComplete()
+            throws Exception {
+
+        // Set up device configuration.
+        configureTrackingEnabled();
+
+        int retriesAllowed = 5;
+        int checkDelayMillis = 5 * 60 * 1000;
+        configureReliabilityConfigSettings(retriesAllowed, checkDelayMillis);
+
+        PackageVersions currentPackageVersions = new PackageVersions(1, 1);
+        PackageVersions newPackageVersions = new PackageVersions(2, 2);
+
+        // Simulate there being a newer version installed than the one recorded in storage.
+        configureValidApplications(currentPackageVersions);
+
+        // Force the storage into a state we want.
+        mPackageStatusStorage.forceCheckStateForTests(
+                PackageStatus.CHECK_COMPLETED_SUCCESS, currentPackageVersions);
+
+        // Initialize the package tracker.
+        mPackageTracker.start();
+
+        // Check the intent helper is properly configured.
+        checkIntentHelperInitializedAndReliabilityTrackingEnabled();
+
+        // Simulate a reliability trigger.
+        simulatePackageInstallation(newPackageVersions);
+
+        // Assert the PackageTracker did trigger an update.
+        checkUpdateCheckTriggered(newPackageVersions);
+        CheckToken token1 = mFakeIntentHelper.captureAndResetLastToken();
+
+        // Increment the clock, but not enough.
+        mFakeClock.incrementClock(checkDelayMillis + 1);
+
+        // Simulate a reliability trigger.
+        mFakeIntentHelper.simulateReliabilityTrigger();
+
+        // Assert the PackageTracker triggered an update.
+        checkUpdateCheckTriggered(newPackageVersions);
+        CheckToken token2 = mFakeIntentHelper.captureAndResetLastToken();
+
+        // Check a new token was generated.
+        assertFalse(token1.equals(token2));
+
+        // Simulate the reliability check completing.
+        mPackageTracker.recordCheckResult(token2, true /* success */);
+
+        // Check everything is now as it should be.
+        checkUpdateCheckSuccessful(newPackageVersions);
+    }
+
+    /**
+     * Simulates a reliability trigger happening too soon after a package update trigger occurred.
+     */
+    @Test
+    public void trackingEnabled_reliabilityTriggerAfterUpdate_tooSoon() throws Exception {
+        // Set up device configuration.
+        configureTrackingEnabled();
+
+        int retriesAllowed = 5;
+        int checkDelayMillis = 5 * 60 * 1000;
+        configureReliabilityConfigSettings(retriesAllowed, checkDelayMillis);
+
+        PackageVersions currentPackageVersions = new PackageVersions(1, 1);
+        PackageVersions newPackageVersions = new PackageVersions(2, 2);
+
+        // Simulate there being a newer version installed than the one recorded in storage.
+        configureValidApplications(currentPackageVersions);
+
+        // Force the storage into a state we want.
+        mPackageStatusStorage.forceCheckStateForTests(
+                PackageStatus.CHECK_COMPLETED_SUCCESS, currentPackageVersions);
+
+        // Initialize the package tracker.
+        mPackageTracker.start();
+
+        // Check the intent helper is properly configured.
+        checkIntentHelperInitializedAndReliabilityTrackingEnabled();
+
+        // Check the initial storage state.
+        checkPackageStorageStatus(PackageStatus.CHECK_COMPLETED_SUCCESS, currentPackageVersions);
+
+        // Simulate a package update trigger.
+        simulatePackageInstallation(newPackageVersions);
+
+        // Assert the PackageTracker did trigger an update.
+        checkUpdateCheckTriggered(newPackageVersions);
+        CheckToken token1 = mFakeIntentHelper.captureAndResetLastToken();
+
+        // Increment the clock, but not enough.
+        mFakeClock.incrementClock(checkDelayMillis - 1);
+
+        // Simulate a reliability trigger.
+        mFakeIntentHelper.simulateReliabilityTrigger();
+
+        // Assert the PackageTracker did not trigger an update.
+        mFakeIntentHelper.assertUpdateNotTriggered();
+        checkPackageStorageStatus(PackageStatus.CHECK_STARTED, newPackageVersions);
+        mFakeIntentHelper.assertReliabilityTriggeringEnabled();
+
+        // Increment the clock slightly more. Should now consider the response overdue.
+        mFakeClock.incrementClock(2);
+
+        // Simulate a reliability trigger.
+        mFakeIntentHelper.simulateReliabilityTrigger();
+
+        // Triggering should have happened.
+        checkUpdateCheckTriggered(newPackageVersions);
+        CheckToken token2 = mFakeIntentHelper.captureAndResetLastToken();
+
+        // Check a new token was generated.
+        assertFalse(token1.equals(token2));
+    }
+
+    private void simulatePackageInstallation(PackageVersions packageVersions) throws Exception {
+        configureApplicationsValidManifests(packageVersions);
+
+        // Simulate a tracked package being updated.
+        mFakeIntentHelper.simulatePackageUpdatedEvent();
+    }
+
+    /**
+     * Checks an update check was triggered, reliability triggering is therefore enabled and the
+     * storage state reflects that there is a check in progress.
+     */
+    private void checkUpdateCheckTriggered(PackageVersions packageVersions) {
+        // Assert the PackageTracker attempted to trigger an update.
+        mFakeIntentHelper.assertUpdateTriggered();
+
+        // If an update check was triggered reliability triggering should always be enabled to
+        // ensure that it can be completed if it fails.
+        mFakeIntentHelper.assertReliabilityTriggeringEnabled();
+
+        // Check the expected storage state.
+        checkPackageStorageStatus(PackageStatus.CHECK_STARTED, packageVersions);
+    }
+
+    private void checkUpdateCheckFailed(PackageVersions packageVersions) {
+        // Check reliability triggering state.
+        mFakeIntentHelper.assertReliabilityTriggeringEnabled();
+
+        // Assert the storage was updated.
+        checkPackageStorageStatus(PackageStatus.CHECK_COMPLETED_FAILURE, packageVersions);
+    }
+
+    private void checkUpdateCheckSuccessful(PackageVersions packageVersions) {
+        // Check reliability triggering state.
+        mFakeIntentHelper.assertReliabilityTriggeringDisabled();
+
+        // Assert the storage was updated.
+        checkPackageStorageStatus(PackageStatus.CHECK_COMPLETED_SUCCESS, packageVersions);
+
+        // Peek inside the package tracker to make sure it is tracking failure counts properly.
+        assertEquals(0, mPackageTracker.getCheckFailureCountForTests());
+    }
+
+    private PackageVersions configureValidApplications() throws Exception {
+        configureValidApplications(INITIAL_APP_PACKAGE_VERSIONS);
+        return INITIAL_APP_PACKAGE_VERSIONS;
+    }
+
+    private void configureValidApplications(PackageVersions versions) throws Exception {
+        configureUpdateAppPackageOk(UPDATE_APP_PACKAGE_NAME);
+        configureDataAppPackageOk(DATA_APP_PACKAGE_NAME);
+        configureApplicationsValidManifests(versions);
+    }
+
+    private void configureApplicationsValidManifests(PackageVersions versions) throws Exception {
+        configureUpdateAppManifestOk(UPDATE_APP_PACKAGE_NAME);
+        configureDataAppManifestOk(DATA_APP_PACKAGE_NAME);
+        configureUpdateAppPackageVersion(UPDATE_APP_PACKAGE_NAME, versions.mUpdateAppVersion);
+        configureDataAppPackageVersion(DATA_APP_PACKAGE_NAME, versions.mDataAppVersion);
+    }
+
+    private void configureUpdateAppPackageVersion(String updateAppPackageName,
+            int updataAppPackageVersion) throws Exception {
+        when(mMockPackageManagerHelper.getInstalledPackageVersion(updateAppPackageName))
+                .thenReturn(updataAppPackageVersion);
+    }
+
+    private void configureDataAppPackageVersion(String dataAppPackageName,
+            int dataAppPackageVersion) throws Exception {
+        when(mMockPackageManagerHelper.getInstalledPackageVersion(dataAppPackageName))
+                .thenReturn(dataAppPackageVersion);
+    }
+
+    private void configureUpdateAppManifestOk(String updateAppPackageName) throws Exception {
+        Intent expectedIntent = RulesUpdaterContract.createUpdaterIntent(updateAppPackageName);
+        when(mMockPackageManagerHelper.receiverRegistered(
+                filterEquals(expectedIntent),
+                eq(RulesUpdaterContract.TRIGGER_TIME_ZONE_RULES_CHECK_PERMISSION)))
+                .thenReturn(true);
+        when(mMockPackageManagerHelper.usesPermission(
+                updateAppPackageName, RulesUpdaterContract.UPDATE_TIME_ZONE_RULES_PERMISSION))
+                .thenReturn(true);
+    }
+
+    private void configureUpdateAppManifestBad(String updateAppPackageName) throws Exception {
+        Intent expectedIntent = RulesUpdaterContract.createUpdaterIntent(updateAppPackageName);
+        when(mMockPackageManagerHelper.receiverRegistered(
+                filterEquals(expectedIntent),
+                eq(RulesUpdaterContract.TRIGGER_TIME_ZONE_RULES_CHECK_PERMISSION)))
+                .thenReturn(false);
+        // Has permission, but that shouldn't matter if the check above is false.
+        when(mMockPackageManagerHelper.usesPermission(
+                updateAppPackageName, RulesUpdaterContract.UPDATE_TIME_ZONE_RULES_PERMISSION))
+                .thenReturn(true);
+    }
+
+    private void configureDataAppManifestOk(String dataAppPackageName) throws Exception {
+        when(mMockPackageManagerHelper.contentProviderRegistered(
+                TimeZoneRulesDataContract.AUTHORITY, dataAppPackageName))
+                .thenReturn(true);
+    }
+
+    private void configureDataAppManifestBad(String dataAppPackageName) throws Exception {
+        // Simulate the data app not exposing the content provider we require.
+        when(mMockPackageManagerHelper.contentProviderRegistered(
+                TimeZoneRulesDataContract.AUTHORITY, dataAppPackageName))
+                .thenReturn(false);
+    }
+
+    private void configureTrackingEnabled() {
+        when(mMockConfigHelper.isTrackingEnabled()).thenReturn(true);
+    }
+
+    private void configureTrackingDisabled() {
+        when(mMockConfigHelper.isTrackingEnabled()).thenReturn(false);
+    }
+
+    private void configureReliabilityConfigSettings(int retriesAllowed, int checkDelayMillis) {
+        when(mMockConfigHelper.getFailedCheckRetryCount()).thenReturn(retriesAllowed);
+        when(mMockConfigHelper.getCheckTimeAllowedMillis()).thenReturn(checkDelayMillis);
+    }
+
+    private void configureReliabilityConfigSettingsOk() {
+        configureReliabilityConfigSettings(5, 5 * 60 * 1000);
+    }
+
+    private void configureUpdateAppPackageOk(String updateAppPackageName) throws Exception {
+        when(mMockConfigHelper.getUpdateAppPackageName()).thenReturn(updateAppPackageName);
+        when(mMockPackageManagerHelper.isPrivilegedApp(updateAppPackageName)).thenReturn(true);
+    }
+
+    private void configureUpdateAppPackageNotPrivileged(String updateAppPackageName)
+            throws Exception {
+        when(mMockConfigHelper.getUpdateAppPackageName()).thenReturn(updateAppPackageName);
+        when(mMockPackageManagerHelper.isPrivilegedApp(updateAppPackageName)).thenReturn(false);
+    }
+
+    private void configureUpdateAppPackageNameMissing() {
+        when(mMockConfigHelper.getUpdateAppPackageName()).thenReturn(null);
+    }
+
+    private void configureDataAppPackageOk(String dataAppPackageName) throws Exception {
+        when(mMockConfigHelper.getDataAppPackageName()).thenReturn(dataAppPackageName);
+        when(mMockPackageManagerHelper.isPrivilegedApp(dataAppPackageName)).thenReturn(true);
+    }
+
+    private void configureDataAppPackageNotPrivileged(String dataAppPackageName)
+            throws Exception {
+        when(mMockConfigHelper.getUpdateAppPackageName()).thenReturn(dataAppPackageName);
+        when(mMockPackageManagerHelper.isPrivilegedApp(dataAppPackageName)).thenReturn(false);
+    }
+
+    private void configureDataAppPackageNameMissing() {
+        when(mMockConfigHelper.getDataAppPackageName()).thenThrow(new RuntimeException());
+    }
+
+    private void checkIntentHelperInitializedAndReliabilityTrackingEnabled() {
+        // Verify that calling start initialized the IntentHelper as well.
+        mFakeIntentHelper.assertInitialized(UPDATE_APP_PACKAGE_NAME, DATA_APP_PACKAGE_NAME);
+
+        // Assert that reliability tracking is always enabled after initialization.
+        mFakeIntentHelper.assertReliabilityTriggeringEnabled();
+    }
+
+    private void checkPackageStorageStatus(
+            int expectedCheckStatus, PackageVersions expectedPackageVersions) {
+        PackageStatus packageStatus = mPackageStatusStorage.getPackageStatus();
+        assertEquals(expectedCheckStatus, packageStatus.mCheckStatus);
+        assertEquals(expectedPackageVersions, packageStatus.mVersions);
+    }
+
+    private void checkPackageStorageStatusIsInitialOrReset() {
+        assertNull(mPackageStatusStorage.getPackageStatus());
+    }
+
+    private static CheckToken createArbitraryCheckToken() {
+        return new CheckToken(1, INITIAL_APP_PACKAGE_VERSIONS);
+    }
+
+    /**
+     * A fake IntentHelper implementation for use in tests.
+     */
+    private static class FakeIntentHelper implements IntentHelper {
+
+        private Listener mListener;
+        private String mUpdateAppPackageName;
+        private String mDataAppPackageName;
+
+        private CheckToken mLastToken;
+
+        private boolean mReliabilityTriggeringEnabled;
+
+        @Override
+        public void initialize(String updateAppPackageName, String dataAppPackageName,
+                Listener listener) {
+            assertNotNull(updateAppPackageName);
+            assertNotNull(dataAppPackageName);
+            assertNotNull(listener);
+            mListener = listener;
+            mUpdateAppPackageName = updateAppPackageName;
+            mDataAppPackageName = dataAppPackageName;
+        }
+
+        public void assertInitialized(
+                String expectedUpdateAppPackageName, String expectedDataAppPackageName) {
+            assertNotNull(mListener);
+            assertEquals(expectedUpdateAppPackageName, mUpdateAppPackageName);
+            assertEquals(expectedDataAppPackageName, mDataAppPackageName);
+        }
+
+        public void assertNotInitialized() {
+            assertNull(mListener);
+        }
+
+        @Override
+        public void sendTriggerUpdateCheck(CheckToken checkToken) {
+            if (mLastToken != null) {
+                fail("lastToken already set");
+            }
+            mLastToken = checkToken;
+        }
+
+        @Override
+        public void enableReliabilityTriggering() {
+            mReliabilityTriggeringEnabled = true;
+        }
+
+        @Override
+        public void disableReliabilityTriggering() {
+            mReliabilityTriggeringEnabled = false;
+        }
+
+        public void assertReliabilityTriggeringEnabled() {
+            assertTrue(mReliabilityTriggeringEnabled);
+        }
+
+        public void assertReliabilityTriggeringDisabled() {
+            assertFalse(mReliabilityTriggeringEnabled);
+        }
+
+        public void assertUpdateTriggered() {
+            assertNotNull(mLastToken);
+        }
+
+        public void assertUpdateNotTriggered() {
+            assertNull(mLastToken);
+        }
+
+        public CheckToken captureAndResetLastToken() {
+            CheckToken toReturn = mLastToken;
+            assertNotNull("No update triggered", toReturn);
+            mLastToken = null;
+            return toReturn;
+        }
+
+        public void simulatePackageUpdatedEvent() {
+            mListener.triggerUpdateIfNeeded(true);
+        }
+
+        public void simulateReliabilityTrigger() {
+            mListener.triggerUpdateIfNeeded(false);
+        }
+    }
+
+    private static class FakeClockHelper implements ClockHelper {
+
+        private long currentTime = 1000;
+
+        @Override
+        public long currentTimestamp() {
+            return currentTime;
+        }
+
+        public void incrementClock(long millis) {
+            currentTime += millis;
+        }
+    }
+
+    /**
+     * Registers a mockito parameter matcher that uses {@link Intent#filterEquals(Intent)}. to
+     * check the parameter against the intent supplied.
+     */
+    private static Intent filterEquals(final Intent expected) {
+        final Matcher<Intent> m = new BaseMatcher<Intent>() {
+            @Override
+            public boolean matches(Object actual) {
+                return actual != null && expected.filterEquals((Intent) actual);
+            }
+            @Override
+            public void describeTo(Description description) {
+                description.appendText(expected.toString());
+            }
+        };
+        return argThat(m);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/timezone/PackageVersionsTest.java b/services/tests/servicestests/src/com/android/server/timezone/PackageVersionsTest.java
new file mode 100644
index 0000000..a470f8f
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/timezone/PackageVersionsTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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 org.junit.Test;
+
+import android.support.test.filters.SmallTest;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+@SmallTest
+public class PackageVersionsTest {
+
+    @Test
+    public void equals() {
+        PackageVersions baseline =
+                new PackageVersions(1 /* updateAppVersion */, 1 /* dataAppVersion */);
+        assertEquals(baseline, baseline);
+
+        PackageVersions deepEqual =
+                new PackageVersions(1 /* updateAppVersion */, 1 /* dataAppVersion */);
+        assertEquals(baseline, deepEqual);
+
+        PackageVersions differentUpdateAppVersion =
+                new PackageVersions(2 /* updateAppVersion */, 1 /* dataAppVersion */);
+        assertFalse(baseline.equals(differentUpdateAppVersion));
+
+        PackageVersions differentDataAppVersion =
+                new PackageVersions(1 /* updateAppVersion */, 2 /* dataAppVersion */);
+        assertFalse(baseline.equals(differentDataAppVersion));
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/timezone/RulesManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/timezone/RulesManagerServiceTest.java
new file mode 100644
index 0000000..a7f4c99
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/timezone/RulesManagerServiceTest.java
@@ -0,0 +1,924 @@
+/*
+ * 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 org.junit.Before;
+import org.junit.Test;
+
+import android.app.timezone.Callback;
+import android.app.timezone.DistroRulesVersion;
+import android.app.timezone.ICallback;
+import android.app.timezone.RulesManager;
+import android.app.timezone.RulesState;
+import android.os.ParcelFileDescriptor;
+
+import java.io.IOException;
+import java.util.concurrent.Executor;
+import javax.annotation.Nullable;
+import libcore.tzdata.shared2.DistroVersion;
+import libcore.tzdata.shared2.StagedDistroOperation;
+import libcore.tzdata.update2.TimeZoneDistroInstaller;
+
+import static com.android.server.timezone.RulesManagerService.REQUIRED_UPDATER_PERMISSION;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+/**
+ * White box interaction / unit testing of the {@link RulesManagerService}.
+ */
+public class RulesManagerServiceTest {
+
+    private RulesManagerService mRulesManagerService;
+
+    private FakeExecutor mFakeExecutor;
+    private PermissionHelper mMockPermissionHelper;
+    private FileDescriptorHelper mMockFileDescriptorHelper;
+    private PackageTracker mMockPackageTracker;
+    private TimeZoneDistroInstaller mMockTimeZoneDistroInstaller;
+
+    @Before
+    public void setUp() {
+        mFakeExecutor = new FakeExecutor();
+
+        mMockFileDescriptorHelper = mock(FileDescriptorHelper.class);
+        mMockPackageTracker = mock(PackageTracker.class);
+        mMockPermissionHelper = mock(PermissionHelper.class);
+        mMockTimeZoneDistroInstaller = mock(TimeZoneDistroInstaller.class);
+
+        mRulesManagerService = new RulesManagerService(
+                mMockPermissionHelper,
+                mFakeExecutor,
+                mMockFileDescriptorHelper,
+                mMockPackageTracker,
+                mMockTimeZoneDistroInstaller);
+    }
+
+    @Test(expected = SecurityException.class)
+    public void getRulesState_noCallerPermission() throws Exception {
+        configureCallerDoesNotHavePermission();
+        mRulesManagerService.getRulesState();
+    }
+
+    @Test(expected = SecurityException.class)
+    public void requestInstall_noCallerPermission() throws Exception {
+        configureCallerDoesNotHavePermission();
+        mRulesManagerService.requestInstall(null, null, null);
+    }
+
+    @Test(expected = SecurityException.class)
+    public void requestUninstall_noCallerPermission() throws Exception {
+        configureCallerDoesNotHavePermission();
+        mRulesManagerService.requestUninstall(null, null);
+    }
+
+    @Test(expected = SecurityException.class)
+    public void requestNothing_noCallerPermission() throws Exception {
+        configureCallerDoesNotHavePermission();
+        mRulesManagerService.requestNothing(null, true);
+    }
+
+    @Test
+    public void getRulesState_systemRulesError() throws Exception {
+        configureDeviceCannotReadSystemRulesVersion();
+
+        assertNull(mRulesManagerService.getRulesState());
+    }
+
+    @Test
+    public void getRulesState_stagedInstall() throws Exception {
+        configureCallerHasPermission();
+
+        configureDeviceSystemRulesVersion("2016a");
+
+        DistroVersion stagedDistroVersion = new DistroVersion(
+                DistroVersion.CURRENT_FORMAT_MAJOR_VERSION,
+                DistroVersion.CURRENT_FORMAT_MINOR_VERSION - 1,
+                "2016c",
+                3);
+        configureStagedInstall(stagedDistroVersion);
+
+        DistroVersion installedDistroVersion = new DistroVersion(
+                DistroVersion.CURRENT_FORMAT_MAJOR_VERSION,
+                DistroVersion.CURRENT_FORMAT_MINOR_VERSION - 1,
+                "2016b",
+                4);
+        configureInstalledDistroVersion(installedDistroVersion);
+
+        DistroRulesVersion stagedDistroRulesVersion = new DistroRulesVersion(
+                stagedDistroVersion.rulesVersion, stagedDistroVersion.revision);
+        DistroRulesVersion installedDistroRulesVersion = new DistroRulesVersion(
+                installedDistroVersion.rulesVersion, installedDistroVersion.revision);
+        RulesState expectedRuleState = new RulesState(
+                "2016a", RulesManagerService.DISTRO_FORMAT_VERSION_SUPPORTED,
+                false /* operationInProgress */,
+                RulesState.STAGED_OPERATION_INSTALL, stagedDistroRulesVersion,
+                RulesState.DISTRO_STATUS_INSTALLED, installedDistroRulesVersion);
+        assertEquals(expectedRuleState, mRulesManagerService.getRulesState());
+    }
+
+    @Test
+    public void getRulesState_nothingStaged() throws Exception {
+        configureCallerHasPermission();
+
+        configureDeviceSystemRulesVersion("2016a");
+
+        configureNoStagedOperation();
+
+        DistroVersion installedDistroVersion = new DistroVersion(
+                DistroVersion.CURRENT_FORMAT_MAJOR_VERSION,
+                DistroVersion.CURRENT_FORMAT_MINOR_VERSION - 1,
+                "2016b",
+                4);
+        configureInstalledDistroVersion(installedDistroVersion);
+
+        DistroRulesVersion installedDistroRulesVersion = new DistroRulesVersion(
+                installedDistroVersion.rulesVersion, installedDistroVersion.revision);
+        RulesState expectedRuleState = new RulesState(
+                "2016a", RulesManagerService.DISTRO_FORMAT_VERSION_SUPPORTED,
+                false /* operationInProgress */,
+                RulesState.STAGED_OPERATION_NONE, null /* stagedDistroRulesVersion */,
+                RulesState.DISTRO_STATUS_INSTALLED, installedDistroRulesVersion);
+        assertEquals(expectedRuleState, mRulesManagerService.getRulesState());
+    }
+
+    @Test
+    public void getRulesState_uninstallStaged() throws Exception {
+        configureCallerHasPermission();
+
+        configureDeviceSystemRulesVersion("2016a");
+
+        configureStagedUninstall();
+
+        DistroVersion installedDistroVersion = new DistroVersion(
+                DistroVersion.CURRENT_FORMAT_MAJOR_VERSION,
+                DistroVersion.CURRENT_FORMAT_MINOR_VERSION - 1,
+                "2016b",
+                4);
+        configureInstalledDistroVersion(installedDistroVersion);
+
+        DistroRulesVersion installedDistroRulesVersion = new DistroRulesVersion(
+                installedDistroVersion.rulesVersion, installedDistroVersion.revision);
+        RulesState expectedRuleState = new RulesState(
+                "2016a", RulesManagerService.DISTRO_FORMAT_VERSION_SUPPORTED,
+                false /* operationInProgress */,
+                RulesState.STAGED_OPERATION_UNINSTALL, null /* stagedDistroRulesVersion */,
+                RulesState.DISTRO_STATUS_INSTALLED, installedDistroRulesVersion);
+        assertEquals(expectedRuleState, mRulesManagerService.getRulesState());
+    }
+
+    @Test
+    public void getRulesState_installedRulesError() throws Exception {
+        configureCallerHasPermission();
+
+        String systemRulesVersion = "2016a";
+        configureDeviceSystemRulesVersion(systemRulesVersion);
+
+        configureStagedUninstall();
+        configureDeviceCannotReadInstalledDistroVersion();
+
+        RulesState expectedRuleState = new RulesState(
+                "2016a", RulesManagerService.DISTRO_FORMAT_VERSION_SUPPORTED,
+                false /* operationInProgress */,
+                RulesState.STAGED_OPERATION_UNINSTALL, null /* stagedDistroRulesVersion */,
+                RulesState.DISTRO_STATUS_UNKNOWN, null /* installedDistroRulesVersion */);
+        assertEquals(expectedRuleState, mRulesManagerService.getRulesState());
+    }
+
+    @Test
+    public void getRulesState_stagedRulesError() throws Exception {
+        configureCallerHasPermission();
+
+        String systemRulesVersion = "2016a";
+        configureDeviceSystemRulesVersion(systemRulesVersion);
+
+        configureDeviceCannotReadStagedDistroOperation();
+
+        DistroVersion installedDistroVersion = new DistroVersion(
+                DistroVersion.CURRENT_FORMAT_MAJOR_VERSION,
+                DistroVersion.CURRENT_FORMAT_MINOR_VERSION - 1,
+                "2016b",
+                4);
+        configureInstalledDistroVersion(installedDistroVersion);
+
+        DistroRulesVersion installedDistroRulesVersion = new DistroRulesVersion(
+                installedDistroVersion.rulesVersion, installedDistroVersion.revision);
+        RulesState expectedRuleState = new RulesState(
+                "2016a", RulesManagerService.DISTRO_FORMAT_VERSION_SUPPORTED,
+                false /* operationInProgress */,
+                RulesState.STAGED_OPERATION_UNKNOWN, null /* stagedDistroRulesVersion */,
+                RulesState.DISTRO_STATUS_INSTALLED, installedDistroRulesVersion);
+        assertEquals(expectedRuleState, mRulesManagerService.getRulesState());
+    }
+
+    @Test
+    public void getRulesState_noInstalledRules() throws Exception {
+        configureCallerHasPermission();
+
+        String systemRulesVersion = "2016a";
+        configureDeviceSystemRulesVersion(systemRulesVersion);
+        configureNoStagedOperation();
+        configureInstalledDistroVersion(null);
+
+        RulesState expectedRuleState = new RulesState(
+                systemRulesVersion, RulesManagerService.DISTRO_FORMAT_VERSION_SUPPORTED,
+                false /* operationInProgress */,
+                RulesState.STAGED_OPERATION_NONE, null /* stagedDistroRulesVersion */,
+                RulesState.DISTRO_STATUS_NONE, null /* installedDistroRulesVersion */);
+        assertEquals(expectedRuleState, mRulesManagerService.getRulesState());
+    }
+
+    @Test
+    public void getRulesState_operationInProgress() throws Exception {
+        configureCallerHasPermission();
+
+        String systemRulesVersion = "2016a";
+        String installedRulesVersion = "2016b";
+        int revision = 3;
+
+        configureDeviceSystemRulesVersion(systemRulesVersion);
+
+        DistroVersion installedDistroVersion = new DistroVersion(
+                DistroVersion.CURRENT_FORMAT_MAJOR_VERSION,
+                DistroVersion.CURRENT_FORMAT_MINOR_VERSION - 1,
+                installedRulesVersion,
+                revision);
+        configureInstalledDistroVersion(installedDistroVersion);
+
+        byte[] expectedContent = createArbitraryBytes(1000);
+        ParcelFileDescriptor parcelFileDescriptor = createFakeParcelFileDescriptor();
+        configureParcelFileDescriptorReadSuccess(parcelFileDescriptor, expectedContent);
+
+        // Start an async operation so there is one in progress. The mFakeExecutor won't actually
+        // execute it.
+        byte[] tokenBytes = createArbitraryTokenBytes();
+        ICallback callback = new StubbedCallback();
+
+        mRulesManagerService.requestInstall(parcelFileDescriptor, tokenBytes, callback);
+
+        RulesState expectedRuleState = new RulesState(
+                systemRulesVersion, RulesManagerService.DISTRO_FORMAT_VERSION_SUPPORTED,
+                true /* operationInProgress */,
+                RulesState.STAGED_OPERATION_UNKNOWN, null /* stagedDistroRulesVersion */,
+                RulesState.DISTRO_STATUS_UNKNOWN, null /* installedDistroRulesVersion */);
+        assertEquals(expectedRuleState, mRulesManagerService.getRulesState());
+    }
+
+    @Test
+    public void requestInstall_operationInProgress() throws Exception {
+        configureCallerHasPermission();
+
+        byte[] expectedContent = createArbitraryBytes(1000);
+        ParcelFileDescriptor parcelFileDescriptor = createFakeParcelFileDescriptor();
+        configureParcelFileDescriptorReadSuccess(parcelFileDescriptor, expectedContent);
+
+        byte[] tokenBytes = createArbitraryTokenBytes();
+        ICallback callback = new StubbedCallback();
+
+        // First request should succeed.
+        assertEquals(RulesManager.SUCCESS,
+                mRulesManagerService.requestInstall(parcelFileDescriptor, tokenBytes, callback));
+
+        // Something async should be enqueued. Clear it but do not execute it so we can detect the
+        // second request does nothing.
+        mFakeExecutor.getAndResetLastCommand();
+
+        // Second request should fail.
+        assertEquals(RulesManager.ERROR_OPERATION_IN_PROGRESS,
+                mRulesManagerService.requestInstall(parcelFileDescriptor, tokenBytes, callback));
+
+        // Assert nothing async was enqueued.
+        mFakeExecutor.assertNothingQueued();
+        verifyNoInstallerCallsMade();
+        verifyNoPackageTrackerCallsMade();
+    }
+
+    @Test
+    public void requestInstall_badToken() throws Exception {
+        configureCallerHasPermission();
+
+        byte[] expectedContent = createArbitraryBytes(1000);
+        ParcelFileDescriptor parcelFileDescriptor = createFakeParcelFileDescriptor();
+        configureParcelFileDescriptorReadSuccess(parcelFileDescriptor, expectedContent);
+
+        byte[] badTokenBytes = new byte[2];
+        ICallback callback = new StubbedCallback();
+
+        try {
+            mRulesManagerService.requestInstall(parcelFileDescriptor, badTokenBytes, callback);
+            fail();
+        } catch (IllegalArgumentException expected) {
+        }
+
+        // Assert nothing async was enqueued.
+        mFakeExecutor.assertNothingQueued();
+        verifyNoInstallerCallsMade();
+        verifyNoPackageTrackerCallsMade();
+    }
+
+    @Test
+    public void requestInstall_nullParcelFileDescriptor() throws Exception {
+        configureCallerHasPermission();
+
+        ParcelFileDescriptor parcelFileDescriptor = null;
+        byte[] tokenBytes = createArbitraryTokenBytes();
+        ICallback callback = new StubbedCallback();
+
+        try {
+            mRulesManagerService.requestInstall(parcelFileDescriptor, tokenBytes, callback);
+            fail();
+        } catch (NullPointerException expected) {}
+
+        // Assert nothing async was enqueued.
+        mFakeExecutor.assertNothingQueued();
+        verifyNoInstallerCallsMade();
+        verifyNoPackageTrackerCallsMade();
+    }
+
+    @Test
+    public void requestInstall_nullCallback() throws Exception {
+        configureCallerHasPermission();
+
+        ParcelFileDescriptor parcelFileDescriptor = createFakeParcelFileDescriptor();
+        byte[] tokenBytes = createArbitraryTokenBytes();
+        ICallback callback = null;
+
+        try {
+            mRulesManagerService.requestInstall(parcelFileDescriptor, tokenBytes, callback);
+            fail();
+        } catch (NullPointerException expected) {}
+
+        // Assert nothing async was enqueued.
+        mFakeExecutor.assertNothingQueued();
+        verifyNoInstallerCallsMade();
+        verifyNoPackageTrackerCallsMade();
+    }
+
+    @Test
+    public void requestInstall_asyncSuccess() throws Exception {
+        configureCallerHasPermission();
+
+        ParcelFileDescriptor parcelFileDescriptor = createFakeParcelFileDescriptor();
+        byte[] expectedContent = createArbitraryBytes(1000);
+        configureParcelFileDescriptorReadSuccess(parcelFileDescriptor, expectedContent);
+
+        CheckToken token = createArbitraryToken();
+        byte[] tokenBytes = token.toByteArray();
+
+        TestCallback callback = new TestCallback();
+
+        // Request the install.
+        assertEquals(RulesManager.SUCCESS,
+                mRulesManagerService.requestInstall(parcelFileDescriptor, tokenBytes, callback));
+
+        // Assert nothing has happened yet.
+        callback.assertNoResultReceived();
+        verifyNoInstallerCallsMade();
+        verifyNoPackageTrackerCallsMade();
+
+        // Set up the installer.
+        configureStageInstallExpectation(expectedContent, TimeZoneDistroInstaller.INSTALL_SUCCESS);
+
+        // Simulate the async execution.
+        mFakeExecutor.simulateAsyncExecutionOfLastCommand();
+
+        // Verify the expected calls were made to other components.
+        verifyStageInstallCalled(expectedContent);
+        verifyPackageTrackerCalled(token, true /* success */);
+
+        // Check the callback was called.
+        callback.assertResultReceived(Callback.SUCCESS);
+    }
+
+    @Test
+    public void requestInstall_nullTokenBytes() throws Exception {
+        configureCallerHasPermission();
+
+        ParcelFileDescriptor parcelFileDescriptor = createFakeParcelFileDescriptor();
+        byte[] expectedContent = createArbitraryBytes(1000);
+        configureParcelFileDescriptorReadSuccess(parcelFileDescriptor, expectedContent);
+
+        TestCallback callback = new TestCallback();
+
+        // Request the install.
+        assertEquals(RulesManager.SUCCESS,
+                mRulesManagerService.requestInstall(
+                        parcelFileDescriptor, null /* tokenBytes */, callback));
+
+        // Assert nothing has happened yet.
+        verifyNoInstallerCallsMade();
+        callback.assertNoResultReceived();
+
+        // Set up the installer.
+        configureStageInstallExpectation(expectedContent, TimeZoneDistroInstaller.INSTALL_SUCCESS);
+
+        // Simulate the async execution.
+        mFakeExecutor.simulateAsyncExecutionOfLastCommand();
+
+        // Verify the expected calls were made to other components.
+        verifyStageInstallCalled(expectedContent);
+        verifyPackageTrackerCalled(null /* expectedToken */, true /* success */);
+
+        // Check the callback was received.
+        callback.assertResultReceived(Callback.SUCCESS);
+    }
+
+    @Test
+    public void requestInstall_asyncInstallFail() throws Exception {
+        configureCallerHasPermission();
+
+        byte[] expectedContent = createArbitraryBytes(1000);
+        ParcelFileDescriptor parcelFileDescriptor = createFakeParcelFileDescriptor();
+        configureParcelFileDescriptorReadSuccess(parcelFileDescriptor, expectedContent);
+
+        CheckToken token = createArbitraryToken();
+        byte[] tokenBytes = token.toByteArray();
+
+        TestCallback callback = new TestCallback();
+
+        // Request the install.
+        assertEquals(RulesManager.SUCCESS,
+                mRulesManagerService.requestInstall(parcelFileDescriptor, tokenBytes, callback));
+
+        // Assert nothing has happened yet.
+        verifyNoInstallerCallsMade();
+        callback.assertNoResultReceived();
+
+        // Set up the installer.
+        configureStageInstallExpectation(
+                expectedContent, TimeZoneDistroInstaller.INSTALL_FAIL_VALIDATION_ERROR);
+
+        // Simulate the async execution.
+        mFakeExecutor.simulateAsyncExecutionOfLastCommand();
+
+        // Verify the expected calls were made to other components.
+        verifyStageInstallCalled(expectedContent);
+
+        // Validation failure is treated like a successful check: repeating it won't improve things.
+        boolean expectedSuccess = true;
+        verifyPackageTrackerCalled(token, expectedSuccess);
+
+        // Check the callback was received.
+        callback.assertResultReceived(Callback.ERROR_INSTALL_VALIDATION_ERROR);
+    }
+
+    @Test
+    public void requestInstall_asyncParcelFileDescriptorReadFail() throws Exception {
+        configureCallerHasPermission();
+
+        ParcelFileDescriptor parcelFileDescriptor = createFakeParcelFileDescriptor();
+        configureParcelFileDescriptorReadFailure(parcelFileDescriptor);
+
+        CheckToken token = createArbitraryToken();
+        byte[] tokenBytes = token.toByteArray();
+
+        TestCallback callback = new TestCallback();
+
+        // Request the install.
+        assertEquals(RulesManager.SUCCESS,
+                mRulesManagerService.requestInstall(parcelFileDescriptor, tokenBytes, callback));
+
+        // Simulate the async execution.
+        mFakeExecutor.simulateAsyncExecutionOfLastCommand();
+
+        // Verify nothing else happened.
+        verifyNoInstallerCallsMade();
+
+        // A failure to read the ParcelFileDescriptor is treated as a failure. It might be the
+        // result of a file system error. This is a fairly arbitrary choice.
+        verifyPackageTrackerCalled(token, false /* success */);
+
+        verifyNoPackageTrackerCallsMade();
+
+        // Check the callback was received.
+        callback.assertResultReceived(Callback.ERROR_UNKNOWN_FAILURE);
+    }
+
+    @Test
+    public void requestUninstall_operationInProgress() throws Exception {
+        configureCallerHasPermission();
+
+        byte[] tokenBytes = createArbitraryTokenBytes();
+        ICallback callback = new StubbedCallback();
+
+        // First request should succeed.
+        assertEquals(RulesManager.SUCCESS,
+                mRulesManagerService.requestUninstall(tokenBytes, callback));
+
+        // Something async should be enqueued. Clear it but do not execute it so we can detect the
+        // second request does nothing.
+        mFakeExecutor.getAndResetLastCommand();
+
+        // Second request should fail.
+        assertEquals(RulesManager.ERROR_OPERATION_IN_PROGRESS,
+                mRulesManagerService.requestUninstall(tokenBytes, callback));
+
+        // Assert nothing async was enqueued.
+        mFakeExecutor.assertNothingQueued();
+        verifyNoInstallerCallsMade();
+        verifyNoPackageTrackerCallsMade();
+    }
+
+    @Test
+    public void requestUninstall_badToken() throws Exception {
+        configureCallerHasPermission();
+
+        byte[] badTokenBytes = new byte[2];
+        ICallback callback = new StubbedCallback();
+
+        try {
+            mRulesManagerService.requestUninstall(badTokenBytes, callback);
+            fail();
+        } catch (IllegalArgumentException expected) {
+        }
+
+        // Assert nothing async was enqueued.
+        mFakeExecutor.assertNothingQueued();
+        verifyNoInstallerCallsMade();
+        verifyNoPackageTrackerCallsMade();
+    }
+
+    @Test
+    public void requestUninstall_nullCallback() throws Exception {
+        configureCallerHasPermission();
+
+        byte[] tokenBytes = createArbitraryTokenBytes();
+        ICallback callback = null;
+
+        try {
+            mRulesManagerService.requestUninstall(tokenBytes, callback);
+            fail();
+        } catch (NullPointerException expected) {}
+
+        // Assert nothing async was enqueued.
+        mFakeExecutor.assertNothingQueued();
+        verifyNoInstallerCallsMade();
+        verifyNoPackageTrackerCallsMade();
+    }
+
+    @Test
+    public void requestUninstall_asyncSuccess() throws Exception {
+        configureCallerHasPermission();
+
+        CheckToken token = createArbitraryToken();
+        byte[] tokenBytes = token.toByteArray();
+
+        TestCallback callback = new TestCallback();
+
+        // Request the uninstall.
+        assertEquals(RulesManager.SUCCESS,
+                mRulesManagerService.requestUninstall(tokenBytes, callback));
+
+        // Assert nothing has happened yet.
+        callback.assertNoResultReceived();
+        verifyNoInstallerCallsMade();
+        verifyNoPackageTrackerCallsMade();
+
+        // Set up the installer.
+        configureStageUninstallExpectation(true /* success */);
+
+        // Simulate the async execution.
+        mFakeExecutor.simulateAsyncExecutionOfLastCommand();
+
+        // Verify the expected calls were made to other components.
+        verifyStageUninstallCalled();
+        verifyPackageTrackerCalled(token, true /* success */);
+
+        // Check the callback was called.
+        callback.assertResultReceived(Callback.SUCCESS);
+    }
+
+    @Test
+    public void requestUninstall_nullTokenBytes() throws Exception {
+        configureCallerHasPermission();
+
+        TestCallback callback = new TestCallback();
+
+        // Request the uninstall.
+        assertEquals(RulesManager.SUCCESS,
+                mRulesManagerService.requestUninstall(null /* tokenBytes */, callback));
+
+        // Assert nothing has happened yet.
+        verifyNoInstallerCallsMade();
+        callback.assertNoResultReceived();
+
+        // Set up the installer.
+        configureStageUninstallExpectation(true /* success */);
+
+        // Simulate the async execution.
+        mFakeExecutor.simulateAsyncExecutionOfLastCommand();
+
+        // Verify the expected calls were made to other components.
+        verifyStageUninstallCalled();
+        verifyPackageTrackerCalled(null /* expectedToken */, true /* success */);
+
+        // Check the callback was received.
+        callback.assertResultReceived(Callback.SUCCESS);
+    }
+
+    @Test
+    public void requestUninstall_asyncUninstallFail() throws Exception {
+        configureCallerHasPermission();
+
+        CheckToken token = createArbitraryToken();
+        byte[] tokenBytes = token.toByteArray();
+
+        TestCallback callback = new TestCallback();
+
+        // Request the uninstall.
+        assertEquals(RulesManager.SUCCESS,
+                mRulesManagerService.requestUninstall(tokenBytes, callback));
+
+        // Assert nothing has happened yet.
+        verifyNoInstallerCallsMade();
+        callback.assertNoResultReceived();
+
+        // Set up the installer.
+        configureStageUninstallExpectation(false /* success */);
+
+        // Simulate the async execution.
+        mFakeExecutor.simulateAsyncExecutionOfLastCommand();
+
+        // Verify the expected calls were made to other components.
+        verifyStageUninstallCalled();
+        verifyPackageTrackerCalled(token, false /* success */);
+
+        // Check the callback was received.
+        callback.assertResultReceived(Callback.ERROR_UNKNOWN_FAILURE);
+    }
+
+    @Test
+    public void requestNothing_operationInProgressOk() throws Exception {
+        configureCallerHasPermission();
+
+        // Set up a parallel operation.
+        assertEquals(RulesManager.SUCCESS,
+                mRulesManagerService.requestUninstall(null, new StubbedCallback()));
+        // Something async should be enqueued. Clear it but do not execute it to simulate it still
+        // being in progress.
+        mFakeExecutor.getAndResetLastCommand();
+
+        CheckToken token = createArbitraryToken();
+        byte[] tokenBytes = token.toByteArray();
+
+        // Make the call.
+        mRulesManagerService.requestNothing(tokenBytes, true /* success */);
+
+        // Assert nothing async was enqueued.
+        mFakeExecutor.assertNothingQueued();
+
+        // Verify the expected calls were made to other components.
+        verifyPackageTrackerCalled(token, true /* success */);
+        verifyNoInstallerCallsMade();
+    }
+
+    @Test
+    public void requestNothing_badToken() throws Exception {
+        configureCallerHasPermission();
+
+        byte[] badTokenBytes = new byte[2];
+
+        try {
+            mRulesManagerService.requestNothing(badTokenBytes, true /* success */);
+            fail();
+        } catch (IllegalArgumentException expected) {
+        }
+
+        // Assert nothing async was enqueued.
+        mFakeExecutor.assertNothingQueued();
+
+        // Assert no other calls were made.
+        verifyNoInstallerCallsMade();
+        verifyNoPackageTrackerCallsMade();
+    }
+
+    @Test
+    public void requestNothing() throws Exception {
+        configureCallerHasPermission();
+
+        CheckToken token = createArbitraryToken();
+        byte[] tokenBytes = token.toByteArray();
+
+        // Make the call.
+        mRulesManagerService.requestNothing(tokenBytes, false /* success */);
+
+        // Assert everything required was done.
+        verifyNoInstallerCallsMade();
+        verifyPackageTrackerCalled(token, false /* success */);
+    }
+
+    @Test
+    public void requestNothing_nullTokenBytes() throws Exception {
+        configureCallerHasPermission();
+
+        // Make the call.
+        mRulesManagerService.requestNothing(null /* tokenBytes */, true /* success */);
+
+        // Assert everything required was done.
+        verifyNoInstallerCallsMade();
+        verifyPackageTrackerCalled(null /* token */, true /* success */);
+    }
+
+    private void verifyNoPackageTrackerCallsMade() {
+        verifyNoMoreInteractions(mMockPackageTracker);
+        reset(mMockPackageTracker);
+    }
+
+    private void verifyPackageTrackerCalled(
+            CheckToken expectedCheckToken, boolean expectedSuccess) {
+        verify(mMockPackageTracker).recordCheckResult(expectedCheckToken, expectedSuccess);
+        reset(mMockPackageTracker);
+    }
+
+    private void configureCallerHasPermission() throws Exception {
+        doNothing()
+                .when(mMockPermissionHelper)
+                .enforceCallerHasPermission(REQUIRED_UPDATER_PERMISSION);
+    }
+
+    private void configureCallerDoesNotHavePermission() {
+        doThrow(new SecurityException("Simulated permission failure"))
+                .when(mMockPermissionHelper)
+                .enforceCallerHasPermission(REQUIRED_UPDATER_PERMISSION);
+    }
+
+    private void configureParcelFileDescriptorReadSuccess(ParcelFileDescriptor parcelFileDescriptor,
+            byte[] content) throws Exception {
+        when(mMockFileDescriptorHelper.readFully(parcelFileDescriptor)).thenReturn(content);
+    }
+
+    private void configureParcelFileDescriptorReadFailure(ParcelFileDescriptor parcelFileDescriptor)
+            throws Exception {
+        when(mMockFileDescriptorHelper.readFully(parcelFileDescriptor))
+                .thenThrow(new IOException("Simulated failure"));
+    }
+
+    private void configureStageInstallExpectation(byte[] expectedContent, int resultCode)
+            throws Exception {
+        when(mMockTimeZoneDistroInstaller.stageInstallWithErrorCode(eq(expectedContent)))
+                .thenReturn(resultCode);
+    }
+
+    private void configureStageUninstallExpectation(boolean success) throws Exception {
+        doReturn(success).when(mMockTimeZoneDistroInstaller).stageUninstall();
+    }
+
+    private void verifyStageInstallCalled(byte[] expectedContent) throws Exception {
+        verify(mMockTimeZoneDistroInstaller).stageInstallWithErrorCode(eq(expectedContent));
+        verifyNoMoreInteractions(mMockTimeZoneDistroInstaller);
+        reset(mMockTimeZoneDistroInstaller);
+    }
+
+    private void verifyStageUninstallCalled() throws Exception {
+        verify(mMockTimeZoneDistroInstaller).stageUninstall();
+        verifyNoMoreInteractions(mMockTimeZoneDistroInstaller);
+        reset(mMockTimeZoneDistroInstaller);
+    }
+
+    private void verifyNoInstallerCallsMade() {
+        verifyNoMoreInteractions(mMockTimeZoneDistroInstaller);
+        reset(mMockTimeZoneDistroInstaller);
+    }
+
+    private static byte[] createArbitraryBytes(int length) {
+        byte[] bytes = new byte[length];
+        for (int i = 0; i < length; i++) {
+            bytes[i] = (byte) i;
+        }
+        return bytes;
+    }
+
+    private byte[] createArbitraryTokenBytes() {
+        return createArbitraryToken().toByteArray();
+    }
+
+    private CheckToken createArbitraryToken() {
+        return new CheckToken(1, new PackageVersions(1, 1));
+    }
+
+    private ParcelFileDescriptor createFakeParcelFileDescriptor() {
+        return new ParcelFileDescriptor((ParcelFileDescriptor) null);
+    }
+
+    private void configureDeviceSystemRulesVersion(String systemRulesVersion) throws Exception {
+        when(mMockTimeZoneDistroInstaller.getSystemRulesVersion()).thenReturn(systemRulesVersion);
+    }
+
+    private void configureInstalledDistroVersion(@Nullable DistroVersion installedDistroVersion)
+            throws Exception {
+        when(mMockTimeZoneDistroInstaller.getInstalledDistroVersion())
+                .thenReturn(installedDistroVersion);
+    }
+
+    private void configureStagedInstall(DistroVersion stagedDistroVersion) throws Exception {
+        when(mMockTimeZoneDistroInstaller.getStagedDistroOperation())
+                .thenReturn(StagedDistroOperation.install(stagedDistroVersion));
+    }
+
+    private void configureStagedUninstall() throws Exception {
+        when(mMockTimeZoneDistroInstaller.getStagedDistroOperation())
+                .thenReturn(StagedDistroOperation.uninstall());
+    }
+
+    private void configureNoStagedOperation() throws Exception {
+        when(mMockTimeZoneDistroInstaller.getStagedDistroOperation()).thenReturn(null);
+    }
+
+    private void configureDeviceCannotReadStagedDistroOperation() throws Exception {
+        when(mMockTimeZoneDistroInstaller.getStagedDistroOperation())
+                .thenThrow(new IOException("Simulated failure"));
+    }
+
+    private void configureDeviceCannotReadSystemRulesVersion() throws Exception {
+        when(mMockTimeZoneDistroInstaller.getSystemRulesVersion())
+                .thenThrow(new IOException("Simulated failure"));
+    }
+
+    private void configureDeviceCannotReadInstalledDistroVersion() throws Exception {
+        when(mMockTimeZoneDistroInstaller.getInstalledDistroVersion())
+                .thenThrow(new IOException("Simulated failure"));
+    }
+
+    private static class FakeExecutor implements Executor {
+
+        private Runnable mLastCommand;
+
+        @Override
+        public void execute(Runnable command) {
+            assertNull(mLastCommand);
+            assertNotNull(command);
+            mLastCommand = command;
+        }
+
+        public Runnable getAndResetLastCommand() {
+            assertNotNull(mLastCommand);
+            Runnable toReturn = mLastCommand;
+            mLastCommand = null;
+            return toReturn;
+        }
+
+        public void simulateAsyncExecutionOfLastCommand() {
+            Runnable toRun = getAndResetLastCommand();
+            toRun.run();
+        }
+
+        public void assertNothingQueued() {
+            assertNull(mLastCommand);
+        }
+    }
+
+    private static class TestCallback extends ICallback.Stub {
+
+        private boolean mOnFinishedCalled;
+        private int mLastError;
+
+        @Override
+        public void onFinished(int error) {
+            assertFalse(mOnFinishedCalled);
+            mOnFinishedCalled = true;
+            mLastError = error;
+        }
+
+        public void assertResultReceived(int expectedResult) {
+            assertTrue(mOnFinishedCalled);
+            assertEquals(expectedResult, mLastError);
+        }
+
+        public void assertNoResultReceived() {
+            assertFalse(mOnFinishedCalled);
+        }
+    }
+
+    private static class StubbedCallback extends ICallback.Stub {
+        @Override
+        public void onFinished(int error) {
+            fail("Unexpected call");
+        }
+    }
+}
diff --git a/telephony/java/com/android/internal/telephony/DctConstants.java b/telephony/java/com/android/internal/telephony/DctConstants.java
index f01e4c0..256e13b 100644
--- a/telephony/java/com/android/internal/telephony/DctConstants.java
+++ b/telephony/java/com/android/internal/telephony/DctConstants.java
@@ -106,6 +106,7 @@
     public static final int EVENT_REDIRECTION_DETECTED = BASE + 44;
     public static final int EVENT_PCO_DATA_RECEIVED = BASE + 45;
     public static final int EVENT_SET_CARRIER_DATA_ENABLED = BASE + 46;
+    public static final int EVENT_DATA_RECONNECT = BASE + 47;
 
     /***** Constants *****/