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 *****/