diff options
17 files changed, 1604 insertions, 4 deletions
diff --git a/Android.mk b/Android.mk index 8bb366ae2fed..deed64232cbc 100644 --- a/Android.mk +++ b/Android.mk @@ -114,6 +114,8 @@ LOCAL_SRC_FILES += \ core/java/android/app/backup/IRestoreObserver.aidl \ core/java/android/app/backup/IRestoreSession.aidl \ core/java/android/app/backup/ISelectBackupTransportCallback.aidl \ + core/java/android/app/timezone/ICallback.aidl \ + core/java/android/app/timezone/IRulesManager.aidl \ core/java/android/app/usage/ICacheQuotaService.aidl \ core/java/android/app/usage/IStorageStatsManager.aidl \ core/java/android/app/usage/IUsageStatsManager.aidl \ diff --git a/core/java/android/app/timezone/Callback.java b/core/java/android/app/timezone/Callback.java new file mode 100644 index 000000000000..b51e5bad0f69 --- /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 000000000000..e879e8f8adbc --- /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 000000000000..5503ce1cf973 --- /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 000000000000..519ef1a86350 --- /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 000000000000..40f3fd22ac6b --- /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 000000000000..649d894ca685 --- /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 000000000000..f789120eb724 --- /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 000000000000..33f4e8060b3e --- /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 000000000000..4e7781866aae --- /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 000000000000..8dd3fb71aa0e --- /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 a8214fac26a2..08c7f186fb69 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -34,7 +34,6 @@ import android.annotation.TestApi; import android.annotation.UserIdInt; import android.app.IApplicationThread; import android.app.IServiceConnection; -import android.app.Notification; import android.app.VrManager; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; @@ -2660,8 +2659,8 @@ public abstract class Context { /** * Similar to {@link #startService(Intent)}, but with an implicit promise that the - * Service will call {@link android.app.Service#startForeground(int, Notification) - * startForeground(int, Notification)} once it begins running. The service is given + * Service will call {@link android.app.Service#startForeground(int, android.app.Notification) + * startForeground(int, android.app.Notification)} once it begins running. The service is given * an amount of time comparable to the ANR interval to do this, otherwise the system * will automatically stop the service and declare the app ANR. * @@ -2682,7 +2681,7 @@ public abstract class Context { * or the service can not be found. * * @see #stopService - * @see android.app.Service#startForeground(int, Notification) + * @see android.app.Service#startForeground(int, android.app.Notification) */ @Nullable public abstract ComponentName startForegroundService(Intent service); @@ -2865,6 +2864,7 @@ public abstract class Context { STORAGE_SERVICE, STORAGE_STATS_SERVICE, WALLPAPER_SERVICE, + TIME_ZONE_RULES_MANAGER_SERVICE, VIBRATOR_SERVICE, //@hide: STATUS_BAR_SERVICE, CONNECTIVITY_SERVICE, @@ -3968,6 +3968,15 @@ public abstract class Context { public static final String VR_SERVICE = "vrmanager"; /** + * Use with {@link #getSystemService} to retrieve an + * {@link android.app.timezone.ITimeZoneRulesManager}. + * @hide + * + * @see #getSystemService + */ + public static final String TIME_ZONE_RULES_MANAGER_SERVICE = "timezone"; + + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. * diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 5e34e0559f79..bae6871a7a93 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -2209,6 +2209,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/tests/coretests/src/android/app/timezone/DistroFormatVersionTest.java b/core/tests/coretests/src/android/app/timezone/DistroFormatVersionTest.java new file mode 100644 index 000000000000..9bbcd3d2d2e9 --- /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 000000000000..2fbc9a1f8e7a --- /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 000000000000..a9357c9175ab --- /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 000000000000..e7a839c3e9fb --- /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); + } +} |