diff --git a/Android.mk b/Android.mk index 1aaa09a40ffde..283e9dc415d40 100644 --- a/Android.mk +++ b/Android.mk @@ -103,6 +103,8 @@ LOCAL_SRC_FILES += \ 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/timezone/Callback.java b/core/java/android/app/timezone/Callback.java new file mode 100644 index 0000000000000..b51e5bad0f69c --- /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 0000000000000..e879e8f8adbc6 --- /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. + * + *

The following properties are included: + *

+ *
majorVersion
+ *
the major distro format version. Major versions differences are not compatible - e.g. + * 2 is not compatible with 1 or 3.
+ *
minorVersion
+ *
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.
+ *
+ * + * @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 CREATOR = new Creator() { + 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 0000000000000..5503ce1cf9736 --- /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. + * + *

The following properties are included: + *

+ *
rulesVersion
+ *
the IANA rules version. e.g. "2017a"
+ *
revision
+ *
the revision for the rules. Allows there to be several revisions for a given IANA rules + * release. Numerically higher is newer.
+ *
+ * + * @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 CREATOR = new Creator() { + 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 0000000000000..519ef1a86350f --- /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 0000000000000..40f3fd22ac6b1 --- /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. + * + *

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. + * + *

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 0000000000000..649d894ca6857 --- /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. + * + *

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. + * + *

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. + * + *

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)}. + * + *

OEMs should not mix the default mechanism and their own as this could lead to conflicts and + * unnecessary checks being triggered. + * + *

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. + * + *

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 0000000000000..f789120eb7248 --- /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 0000000000000..33f4e8060b3e6 --- /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. + * + *

The following properties are included: + *

+ *
systemRulesVersion
+ *
the IANA rules version that shipped with the OS. Always present. e.g. "2017a".
+ *
distroFormatVersionSupported
+ *
the distro format version supported by this device. Always present.
+ *
operationInProgress
+ *
{@code true} if there is an install / uninstall operation currently happening.
+ *
stagedOperationType
+ *
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.
+ *
stagedDistroRulesVersion
+ *
[present if distroStagedState == STAGED_STATE_INSTALL], the rules version of the distro + * currently staged for installation.
+ *
distroStatus
+ *
{@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}. + *
+ *
installedDistroRulesVersion
+ *
[present if distroStatus == {@link #DISTRO_STATUS_INSTALLED}], the rules version of the + * installed and active distro.
+ *
+ * + * @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 CREATOR = + new Parcelable.Creator() { + 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 0000000000000..4e7781866aae0 --- /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 0000000000000..8dd3fb71aa0e4 --- /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 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 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 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 fed36b065d610..25128542ae0e7 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -2656,6 +2656,7 @@ public abstract class Context { SENSOR_SERVICE, STORAGE_SERVICE, WALLPAPER_SERVICE, + TIME_ZONE_RULES_MANAGER_SERVICE, VIBRATOR_SERVICE, //@hide: STATUS_BAR_SERVICE, CONNECTIVITY_SERVICE, @@ -3667,6 +3668,15 @@ public abstract class Context { */ 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/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 9991a20a9b82d..f43eb848f2df3 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -2084,6 +2084,22 @@ + + + + + + + m = new BaseMatcher() { + @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); + } +}