OSDN Git Service

Time zone update API classes
authorNeil Fuller <nfuller@google.com>
Thu, 16 Mar 2017 18:29:36 +0000 (18:29 +0000)
committerNeil Fuller <nfuller@google.com>
Wed, 3 May 2017 16:41:12 +0000 (17:41 +0100)
Time zone update API classes. Currently hidden but they
will go on to be unhidden in future.

This forms the basis of client and system server code to
support time zone updates.

Tests can be run with:

make -j30 FrameworksCoreTests
adb install out/target/product/angler/data/app/FrameworksCoreTests/FrameworksCoreTests.apk
adb shell am instrument -e package android.app.timezone \
    -w com.android.frameworks.coretests \
    "com.android.frameworks.coretests/android.support.test.runner.AndroidJUnitRunner"

Bug: 31008728
Test: See above.
Change-Id: I668bc8ac0cb78f3d4d9a4b7ad6ac1534b88af833

17 files changed:
Android.mk
core/java/android/app/timezone/Callback.java [new file with mode: 0644]
core/java/android/app/timezone/DistroFormatVersion.java [new file with mode: 0644]
core/java/android/app/timezone/DistroRulesVersion.java [new file with mode: 0644]
core/java/android/app/timezone/ICallback.aidl [new file with mode: 0644]
core/java/android/app/timezone/IRulesManager.aidl [new file with mode: 0644]
core/java/android/app/timezone/RulesManager.java [new file with mode: 0644]
core/java/android/app/timezone/RulesState.aidl [new file with mode: 0644]
core/java/android/app/timezone/RulesState.java [new file with mode: 0644]
core/java/android/app/timezone/RulesUpdaterContract.java [new file with mode: 0644]
core/java/android/app/timezone/Utils.java [new file with mode: 0644]
core/java/android/content/Context.java
core/res/AndroidManifest.xml
core/tests/coretests/src/android/app/timezone/DistroFormatVersionTest.java [new file with mode: 0644]
core/tests/coretests/src/android/app/timezone/DistroRulesVersionTest.java [new file with mode: 0644]
core/tests/coretests/src/android/app/timezone/RulesStateTest.java [new file with mode: 0644]
core/tests/coretests/src/android/app/timezone/RulesUpdaterContractTest.java [new file with mode: 0644]

index 8bb366a..deed642 100644 (file)
@@ -114,6 +114,8 @@ LOCAL_SRC_FILES += \
        core/java/android/app/backup/IRestoreObserver.aidl \
        core/java/android/app/backup/IRestoreSession.aidl \
        core/java/android/app/backup/ISelectBackupTransportCallback.aidl \
+       core/java/android/app/timezone/ICallback.aidl \
+       core/java/android/app/timezone/IRulesManager.aidl \
        core/java/android/app/usage/ICacheQuotaService.aidl \
        core/java/android/app/usage/IStorageStatsManager.aidl \
        core/java/android/app/usage/IUsageStatsManager.aidl \
diff --git a/core/java/android/app/timezone/Callback.java b/core/java/android/app/timezone/Callback.java
new file mode 100644 (file)
index 0000000..b51e5ba
--- /dev/null
@@ -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 (file)
index 0000000..e879e8f
--- /dev/null
@@ -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 (file)
index 0000000..5503ce1
--- /dev/null
@@ -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 (file)
index 0000000..519ef1a
--- /dev/null
@@ -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 (file)
index 0000000..40f3fd2
--- /dev/null
@@ -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 (file)
index 0000000..649d894
--- /dev/null
@@ -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 (file)
index 0000000..f789120
--- /dev/null
@@ -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 (file)
index 0000000..33f4e80
--- /dev/null
@@ -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 (file)
index 0000000..4e77818
--- /dev/null
@@ -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 (file)
index 0000000..8dd3fb7
--- /dev/null
@@ -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;
+    }
+}
index a8214fa..08c7f18 100644 (file)
@@ -34,7 +34,6 @@ import android.annotation.TestApi;
 import android.annotation.UserIdInt;
 import android.app.IApplicationThread;
 import android.app.IServiceConnection;
-import android.app.Notification;
 import android.app.VrManager;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
@@ -2660,8 +2659,8 @@ public abstract class Context {
 
     /**
      * Similar to {@link #startService(Intent)}, but with an implicit promise that the
-     * Service will call {@link android.app.Service#startForeground(int, Notification)
-     * startForeground(int, Notification)} once it begins running.  The service is given
+     * Service will call {@link android.app.Service#startForeground(int, android.app.Notification)
+     * startForeground(int, android.app.Notification)} once it begins running.  The service is given
      * an amount of time comparable to the ANR interval to do this, otherwise the system
      * will automatically stop the service and declare the app ANR.
      *
@@ -2682,7 +2681,7 @@ public abstract class Context {
      * or the service can not be found.
      *
      * @see #stopService
-     * @see android.app.Service#startForeground(int, Notification)
+     * @see android.app.Service#startForeground(int, android.app.Notification)
      */
     @Nullable
     public abstract ComponentName startForegroundService(Intent service);
@@ -2865,6 +2864,7 @@ public abstract class Context {
             STORAGE_SERVICE,
             STORAGE_STATS_SERVICE,
             WALLPAPER_SERVICE,
+            TIME_ZONE_RULES_MANAGER_SERVICE,
             VIBRATOR_SERVICE,
             //@hide: STATUS_BAR_SERVICE,
             CONNECTIVITY_SERVICE,
@@ -3968,6 +3968,15 @@ public abstract class Context {
     public static final String VR_SERVICE = "vrmanager";
 
     /**
+     * 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.
      *
index 5e34e05..bae6871 100644 (file)
     <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/tests/coretests/src/android/app/timezone/DistroFormatVersionTest.java b/core/tests/coretests/src/android/app/timezone/DistroFormatVersionTest.java
new file mode 100644 (file)
index 0000000..9bbcd3d
--- /dev/null
@@ -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 (file)
index 0000000..2fbc9a1
--- /dev/null
@@ -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 (file)
index 0000000..a9357c9
--- /dev/null
@@ -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 (file)
index 0000000..e7a839c
--- /dev/null
@@ -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);
+    }
+}